/** * Leaderboard component for Golf game. * Handles leaderboard display, metric switching, and player stats modal. */ class LeaderboardComponent { constructor() { this.currentMetric = 'wins'; this.cache = new Map(); this.cacheTimeout = 60000; // 1 minute cache this.elements = { screen: document.getElementById('leaderboard-screen'), backBtn: document.getElementById('leaderboard-back-btn'), openBtn: document.getElementById('leaderboard-btn'), tabs: document.getElementById('leaderboard-tabs'), content: document.getElementById('leaderboard-content'), statsModal: document.getElementById('player-stats-modal'), statsContent: document.getElementById('player-stats-content'), statsClose: document.getElementById('player-stats-close'), }; this.metricLabels = { wins: 'Total Wins', win_rate: 'Win Rate', avg_score: 'Avg Score', knockouts: 'Knockouts', streak: 'Best Streak', }; this.metricFormats = { wins: (v) => v.toLocaleString(), win_rate: (v) => `${v.toFixed(1)}%`, avg_score: (v) => v.toFixed(1), knockouts: (v) => v.toLocaleString(), streak: (v) => v.toLocaleString(), }; this.init(); } init() { // Open leaderboard this.elements.openBtn?.addEventListener('click', () => this.show()); // Back button this.elements.backBtn?.addEventListener('click', () => this.hide()); // Tab switching this.elements.tabs?.addEventListener('click', (e) => { if (e.target.classList.contains('leaderboard-tab')) { this.switchMetric(e.target.dataset.metric); } }); // Close player stats modal this.elements.statsClose?.addEventListener('click', () => this.closePlayerStats()); this.elements.statsModal?.addEventListener('click', (e) => { if (e.target === this.elements.statsModal) { this.closePlayerStats(); } }); // Handle clicks on player names this.elements.content?.addEventListener('click', (e) => { const playerLink = e.target.closest('.player-link'); if (playerLink) { const userId = playerLink.dataset.userId; if (userId) { this.showPlayerStats(userId); } } }); } show() { // Hide other screens, show leaderboard document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); this.elements.screen.classList.add('active'); this.loadLeaderboard(this.currentMetric); } hide() { this.elements.screen.classList.remove('active'); document.getElementById('lobby-screen').classList.add('active'); } switchMetric(metric) { if (metric === this.currentMetric) return; this.currentMetric = metric; // Update tab styling this.elements.tabs.querySelectorAll('.leaderboard-tab').forEach(tab => { tab.classList.toggle('active', tab.dataset.metric === metric); }); this.loadLeaderboard(metric); } async loadLeaderboard(metric) { // Check cache const cacheKey = `leaderboard_${metric}`; const cached = this.cache.get(cacheKey); if (cached && Date.now() - cached.time < this.cacheTimeout) { this.renderLeaderboard(cached.data, metric); return; } // Show loading this.elements.content.innerHTML = '
Loading...
'; try { const response = await fetch(`/api/stats/leaderboard?metric=${metric}&limit=50`); if (!response.ok) throw new Error('Failed to load leaderboard'); const data = await response.json(); // Cache the result this.cache.set(cacheKey, { data, time: Date.now() }); this.renderLeaderboard(data, metric); } catch (error) { console.error('Error loading leaderboard:', error); this.elements.content.innerHTML = `

Failed to load leaderboard

`; } } renderLeaderboard(data, metric) { const entries = data.entries || []; if (entries.length === 0) { this.elements.content.innerHTML = `

No players on the leaderboard yet.

Play 5+ games to appear here!

`; return; } const formatValue = this.metricFormats[metric] || (v => v); const currentUserId = this.getCurrentUserId(); let html = ` `; entries.forEach(entry => { const isMe = entry.user_id === currentUserId; const medal = this.getMedal(entry.rank); html += ` `; }); html += '
# Player ${this.metricLabels[metric]} Games
${medal || entry.rank} ${this.escapeHtml(entry.username)}${isMe ? ' (you)' : ''} ${formatValue(entry.value)} ${entry.games_played}
'; this.elements.content.innerHTML = html; } getMedal(rank) { switch (rank) { case 1: return '🥇'; case 2: return '🥈'; case 3: return '🥉'; default: return null; } } async showPlayerStats(userId) { this.elements.statsModal.classList.remove('hidden'); this.elements.statsContent.innerHTML = '
Loading...
'; try { const [statsRes, achievementsRes] = await Promise.all([ fetch(`/api/stats/players/${userId}`), fetch(`/api/stats/players/${userId}/achievements`), ]); if (!statsRes.ok) throw new Error('Failed to load player stats'); const stats = await statsRes.json(); const achievements = achievementsRes.ok ? await achievementsRes.json() : { achievements: [] }; this.renderPlayerStats(stats, achievements.achievements || []); } catch (error) { console.error('Error loading player stats:', error); this.elements.statsContent.innerHTML = `

Failed to load player stats

`; } } renderPlayerStats(stats, achievements) { const currentUserId = this.getCurrentUserId(); const isMe = stats.user_id === currentUserId; let html = `

${this.escapeHtml(stats.username)}${isMe ? ' (you)' : ''}

${stats.games_played >= 5 ? '

Ranked Player

' : '

Unranked (needs 5+ games)

'}
${stats.games_won}
Wins
${stats.win_rate.toFixed(1)}%
Win Rate
${stats.games_played}
Games
${stats.avg_score.toFixed(1)}
Avg Score
${stats.best_round_score ?? '-'}
Best Round
${stats.knockouts}
Knockouts
${stats.best_win_streak}
Best Streak
${stats.rounds_played}
Rounds
`; // Achievements section if (achievements.length > 0) { html += `

Achievements (${achievements.length})

`; achievements.forEach(a => { html += `
${a.icon} ${this.escapeHtml(a.name)}
`; }); html += '
'; } this.elements.statsContent.innerHTML = html; } closePlayerStats() { this.elements.statsModal.classList.add('hidden'); } getCurrentUserId() { // Get user ID from auth state if available if (window.authState && window.authState.user) { return window.authState.user.id; } return null; } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Public method to clear cache (e.g., after game ends) clearCache() { this.cache.clear(); } } // Initialize global leaderboard instance const leaderboard = new LeaderboardComponent();