/** * Gamification features for Spelling Practice App * - Leaderboards * - Challenges & Achievements * - Streaks * - Points system */ // Global variables for gamification let userProgress = null; let challenges = null; let leaderboards = null; let currentUser = null; // Initialize gamification features document.addEventListener('DOMContentLoaded', function() { // Event listener will be triggered when a user logs in document.addEventListener('userLoggedIn', function(e) { currentUser = e.detail.username; initGamification(currentUser); }); }); // Main initialization function for gamification async function initGamification(username) { try { // Fetch all required data await Promise.all([ fetchUserProgress(username), fetchChallenges(), fetchLeaderboards() ]); // Update streak (marks user as active today) await updateUserStreak(username); // Render all gamification components renderStreakIndicator(); renderUserStats(); renderChallenges(); renderLeaderboards(); console.log('🎮 Gamification features initialized'); } catch (error) { console.error('Error initializing gamification:', error); } } // Fetch user progress data async function fetchUserProgress(username) { try { const response = await fetch(`/getUserProgress?username=${encodeURIComponent(username)}`); if (!response.ok) { throw new Error(`Failed to fetch user progress: ${response.status}`); } userProgress = await response.json(); return userProgress; } catch (error) { console.error('Error fetching user progress:', error); return null; } } // Fetch challenges data async function fetchChallenges() { try { const response = await fetch('/getChallenges'); if (!response.ok) { throw new Error(`Failed to fetch challenges: ${response.status}`); } challenges = await response.json(); return challenges; } catch (error) { console.error('Error fetching challenges:', error); return null; } } // Fetch leaderboards data async function fetchLeaderboards() { try { const response = await fetch('/getLeaderboards'); if (!response.ok) { throw new Error(`Failed to fetch leaderboards: ${response.status}`); } leaderboards = await response.json(); return leaderboards; } catch (error) { console.error('Error fetching leaderboards:', error); return null; } } // Update user streak async function updateUserStreak(username) { try { const response = await fetch('/updateStreak', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username }) }); if (!response.ok) { throw new Error(`Failed to update streak: ${response.status}`); } const result = await response.json(); // Update local data if (userProgress) { userProgress.streaks = result.streak; } return result; } catch (error) { console.error('Error updating streak:', error); return null; } } // Complete a challenge async function completeChallenge(challengeId, challengeType) { if (!currentUser) { console.error('No user logged in'); return; } try { const response = await fetch('/completeChallenge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: currentUser, challengeId, challengeType }) }); if (!response.ok) { throw new Error(`Failed to complete challenge: ${response.status}`); } const result = await response.json(); // Update local data userProgress = result.updatedProgress; // Update UI renderUserStats(); renderChallenges(); // Show success message showToast('Challenge completed!', 'success'); return result; } catch (error) { console.error('Error completing challenge:', error); showToast('Failed to complete challenge', 'error'); return null; } } // Render streak indicator in student panel function renderStreakIndicator() { const streakContainer = document.getElementById('streakContainer'); if (!streakContainer) return; // Clear previous content streakContainer.innerHTML = ''; const currentStreak = userProgress?.streaks?.current || 0; // Create streak element const streakEl = document.createElement('div'); streakEl.className = 'streak-container'; streakEl.innerHTML = `
🔥
${currentStreak} Day${currentStreak === 1 ? '' : 's'}
${currentStreak > 0 ? 'Current streak' : 'Start your streak today!'}
${Array.from({ length: 7 }, (_, i) => `
` ).join('')}
Best
${userProgress?.streaks?.longest || 0}
`; streakContainer.appendChild(streakEl); } // Render user stats in student panel function renderUserStats() { const statsContainer = document.getElementById('userStatsContainer'); if (!statsContainer) return; // Clear previous content statsContainer.innerHTML = ''; // Create stats container const statsEl = document.createElement('div'); statsEl.className = 'user-stats'; // Add stat cards statsEl.innerHTML = `
${userProgress?.stats?.points || 0}
Points
${userProgress?.stats?.accuracy ? userProgress.stats.accuracy.toFixed(1) + '%' : '0%'}
Accuracy
${userProgress?.stats?.totalSessions || 0}
Sessions
${userProgress?.stats?.totalWords || 0}
Words
`; statsContainer.appendChild(statsEl); } // Initialize containers with default content function initializeStudentContainers() { // Initialize streak container immediately const streakContainer = document.getElementById('streakContainer'); if (streakContainer && !streakContainer.innerHTML.trim()) { renderStreakIndicator(); } // Initialize stats container immediately const statsContainer = document.getElementById('userStatsContainer'); if (statsContainer && !statsContainer.innerHTML.trim()) { renderUserStats(); } } // Render challenges in student panel function renderChallenges() { const challengesContainer = document.getElementById('challengesContainer'); if (!challengesContainer || !challenges || !userProgress) return; // Clear previous content challengesContainer.innerHTML = ''; // Create challenges container const challengesEl = document.createElement('div'); challengesEl.className = 'challenges-container'; // Daily challenge if (challenges.daily && challenges.daily.current) { const dailyChallenge = challenges.daily.current; const isCompleted = userProgress.challengesCompleted.daily.includes(dailyChallenge.id); challengesEl.appendChild(createChallengeCard(dailyChallenge, 'daily', isCompleted)); } // Weekly challenge if (challenges.weekly && challenges.weekly.current) { const weeklyChallenge = challenges.weekly.current; const isCompleted = userProgress.challengesCompleted.weekly.includes(weeklyChallenge.id); challengesEl.appendChild(createChallengeCard(weeklyChallenge, 'weekly', isCompleted)); } // Achievement challenges (show only 3 unearned ones) if (challenges.achievements && challenges.achievements.length > 0) { const uncompletedAchievements = challenges.achievements .filter(achievement => !userProgress.challengesCompleted.achievements.includes(achievement.id)) .slice(0, 3); uncompletedAchievements.forEach(achievement => { challengesEl.appendChild(createChallengeCard(achievement, 'achievement', false)); }); } challengesContainer.appendChild(challengesEl); } // Create a challenge card element function createChallengeCard(challenge, type, isCompleted) { const card = document.createElement('div'); card.className = `challenge-card ${type}`; card.dataset.id = challenge.id; // Calculate progress for the challenge let progress = 0; let progressText = ''; switch (challenge.type) { case 'accuracy': progress = userProgress.stats.accuracy ? Math.min(userProgress.stats.accuracy / challenge.target, 1) : 0; progressText = `${userProgress.stats.accuracy ? userProgress.stats.accuracy.toFixed(1) : 0}% / ${challenge.target}%`; break; case 'words': case 'correctWords': progress = Math.min(userProgress.stats.correctWords / challenge.target, 1); progressText = `${userProgress.stats.correctWords || 0} / ${challenge.target}`; break; case 'sessions': progress = Math.min(userProgress.stats.totalSessions / challenge.target, 1); progressText = `${userProgress.stats.totalSessions || 0} / ${challenge.target}`; break; case 'streak': progress = Math.min(userProgress.streaks.current / challenge.target, 1); progressText = `${userProgress.streaks.current || 0} / ${challenge.target}`; break; case 'perfectSessions': // Would need to track this separately progress = 0; progressText = `0 / ${challenge.target}`; break; } // Format date for challenge time period let timeframe = ''; if (type === 'daily') { timeframe = `Today (${new Date(challenge.date).toLocaleDateString()})`; } else if (type === 'weekly') { timeframe = `${new Date(challenge.startDate).toLocaleDateString()} - ${new Date(challenge.endDate).toLocaleDateString()}`; } card.innerHTML = `

${challenge.title}

${ type === 'daily' ? 'Daily' : type === 'weekly' ? 'Weekly' : 'Achievement' }
${challenge.description}
${progressText} ${Math.round(progress * 100)}%
`; return card; } // Render leaderboards in student panel function renderLeaderboards() { const leaderboardsContainer = document.getElementById('leaderboardsContainer'); if (!leaderboardsContainer || !leaderboards) return; // Clear previous content leaderboardsContainer.innerHTML = ''; // Create leaderboard container const leaderboardEl = document.createElement('div'); leaderboardEl.className = 'leaderboard-container'; // Create header with tabs const header = document.createElement('div'); header.className = 'leaderboard-header'; header.innerHTML = `

Leaderboards

Accuracy
Words
Streaks
`; leaderboardEl.appendChild(header); // Create leaderboard table container const tableContainer = document.createElement('div'); tableContainer.id = 'leaderboardTableContainer'; // Initially show accuracy leaderboard tableContainer.appendChild(createLeaderboardTable('accuracy')); leaderboardEl.appendChild(tableContainer); leaderboardsContainer.appendChild(leaderboardEl); // Add event listeners for tabs const tabs = header.querySelectorAll('.leaderboard-tab'); tabs.forEach(tab => { tab.addEventListener('click', function() { // Remove active class from all tabs tabs.forEach(t => t.classList.remove('active')); // Add active class to clicked tab this.classList.add('active'); // Update table const category = this.dataset.category; tableContainer.innerHTML = ''; tableContainer.appendChild(createLeaderboardTable(category)); }); }); } // Create a leaderboard table for a specific category function createLeaderboardTable(category) { const table = document.createElement('table'); table.className = 'leaderboard-table'; // Create table header const thead = document.createElement('thead'); thead.innerHTML = ` # User ${ category === 'accuracy' ? 'Accuracy' : category === 'totalWords' ? 'Words' : category === 'sessionsCompleted' ? 'Sessions' : 'Streak Days' } `; table.appendChild(thead); // Create table body const tbody = document.createElement('tbody'); // Get leaderboard data const data = leaderboards.allTime[category] || []; if (data.length === 0) { const tr = document.createElement('tr'); tr.innerHTML = 'No data available'; tbody.appendChild(tr); } else { data.forEach((entry, index) => { const tr = document.createElement('tr'); tr.innerHTML = ` ${index + 1} ${entry.username}${entry.username === currentUser ? ' (You)' : ''} ${ category === 'accuracy' ? entry.value.toFixed(1) + '%' : entry.value } `; // Highlight current user if (entry.username === currentUser) { tr.style.backgroundColor = 'var(--primary-soft)'; } tbody.appendChild(tr); }); } table.appendChild(tbody); return table; } // Helper function to show toast messages function showToast(message, type = 'info') { // Check if toast container exists let toastContainer = document.querySelector('.toast-container'); if (!toastContainer) { toastContainer = document.createElement('div'); toastContainer.className = 'toast-container'; document.body.appendChild(toastContainer); } // Create toast const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; // Add to container toastContainer.appendChild(toast); // Show toast with animation setTimeout(() => toast.classList.add('show'), 10); // Remove after delay setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, 3000); } // Expose functions to window for use in HTML window.completeChallenge = completeChallenge;