// Predictions Page - Prediction leaderboard, open matches, and submission import type { PredictorStats, OpenMatch, BotProfile } from '../api-types'; import { fetchPredictionsLeaderboard, fetchOpenPredictions, submitPrediction, getOrCreatePredictorId, fetchPredictionHistory, } from '../api-types'; const PAGES_BASE = ''; let openMatches: OpenMatch[] = []; let pollTimer: ReturnType | null = null; let predictorId = ''; export async function renderPredictionsPage(): Promise { const app = document.getElementById('app'); if (!app) return; predictorId = getOrCreatePredictorId(); app.innerHTML = `

Predictions

Predict match outcomes and climb the leaderboard

How It Works

Predict the winner of upcoming matches before they start. The more accurate your predictions, the higher you climb the leaderboard.

1

Make a Pick

Choose which bot you think will win a match

2

Wait for Result

After the match completes, predictions are resolved

3

Climb the Ranks

Correct predictions increase your streak and ranking

Open Matches

Loading open matches...

Your Predictions

Loading your predictions...

Top Predictors

Loading leaderboard...

Your Stats

Make your first prediction above to start tracking stats

`; // Load open matches, leaderboard, and history in parallel await Promise.all([loadOpenMatches(), loadLeaderboard(), loadHistory()]); // Poll for resolved predictions every 15 seconds pollTimer = setInterval(async () => { await Promise.all([loadOpenMatches(), loadHistory()]); }, 15000); } // Cleanup polling when navigating away (called by SPA router) export function cleanupPredictionsPage(): void { if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } } async function loadOpenMatches(): Promise { const container = document.getElementById('open-matches-container'); if (!container) return; try { const data = await fetchOpenPredictions(predictorId); openMatches = data.matches || []; if (openMatches.length === 0) { container.innerHTML = '
No open matches available for prediction right now. Check back soon!
'; return; } container.innerHTML = openMatches.map(m => { const participants = m.participants || []; if (participants.length < 2) return ''; const botA = participants[0]; const botB = participants[1]; const pickedA = m.your_pick === botA.bot_id; const pickedB = m.your_pick === botB.bot_id; return `
${escapeHtml(botA.name)}
Rating: ${Math.round(botA.rating)}
vs
${escapeHtml(botB.name)}
Rating: ${Math.round(botB.rating)}
`; }).join(''); // Attach click handlers container.querySelectorAll('.pick-btn:not(.picked)').forEach(btn => { btn.addEventListener('click', handlePick); }); } catch (err) { console.error('Failed to load open matches:', err); container.innerHTML = '
Failed to load open matches
'; } } async function handlePick(e: Event): Promise { const btn = e.target as HTMLButtonElement; const matchId = btn.getAttribute('data-match')!; const botId = btn.getAttribute('data-bot')!; const card = btn.closest('.open-match-card') as HTMLElement; // Disable all buttons in this card card.querySelectorAll('.pick-btn').forEach(b => { (b as HTMLButtonElement).disabled = true; }); btn.textContent = 'Submitting...'; try { await submitPrediction(matchId, botId, predictorId); // Mark the picked button btn.textContent = 'Picked'; btn.classList.add('picked'); // Update the other button to show it wasn't picked card.querySelectorAll('.pick-btn:not(.picked)').forEach(b => { (b as HTMLButtonElement).textContent = 'Not picked'; }); // Refresh history to show the new prediction loadHistory(); } catch (err) { console.error('Failed to submit prediction:', err); btn.textContent = 'Error'; card.querySelectorAll('.pick-btn').forEach(b => { (b as HTMLButtonElement).disabled = false; }); // Show error message const errDiv = card.querySelector('.prediction-error'); if (errDiv) errDiv.textContent = (err as Error).message; } } async function loadHistory(): Promise { const container = document.getElementById('history-container'); if (!container) return; try { const data = await fetchPredictionHistory(predictorId, 20); const predictions = data.predictions || []; if (predictions.length === 0) { container.innerHTML = '
You haven\'t made any predictions yet. Pick a bot above!
'; return; } container.innerHTML = predictions.map(p => { let icon: string, iconClass: string, statusText: string, statusClass: string; if (p.correct === true) { icon = '✓'; iconClass = 'correct'; statusText = 'Correct!'; statusClass = 'correct'; } else if (p.correct === false) { icon = '✗'; iconClass = 'incorrect'; statusText = p.winner_name ? `Wrong — ${p.winner_name} won` : 'Wrong'; statusClass = 'incorrect'; } else { icon = '…'; iconClass = 'pending'; statusText = 'Pending'; statusClass = 'pending'; } return `
${icon}
Picked ${escapeHtml(p.predicted_name || p.predicted_bot)}
${formatTimeAgo(p.created_at)}
${statusText}
`; }).join(''); } catch (err) { console.error('Failed to load prediction history:', err); container.innerHTML = '
Failed to load prediction history
'; } } function formatTimeAgo(isoString: string): string { const date = new Date(isoString); const seconds = Math.floor((Date.now() - date.getTime()) / 1000); if (seconds < 60) return 'just now'; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); return `${days}d ago`; } async function loadLeaderboard(): Promise { const container = document.getElementById('leaderboard-container'); if (!container) return; try { const data = await fetchPredictionsLeaderboard(); if (data.entries.length === 0) { container.innerHTML = '
No predictions have been made yet
'; return; } // Check if current predictor is in the list const myEntry = data.entries.find((e: PredictorStats) => e.predictor_id === predictorId); if (myEntry) { const statsEl = document.getElementById('your-stats'); const promptEl = document.getElementById('stats-login-prompt'); if (statsEl && promptEl) { statsEl.style.display = 'block'; promptEl.style.display = 'none'; const total = myEntry.correct + myEntry.incorrect; const accuracy = total > 0 ? Math.round((myEntry.correct / total) * 100) : 0; document.getElementById('stat-total')!.textContent = String(total); document.getElementById('stat-accuracy')!.textContent = `${accuracy}%`; document.getElementById('stat-streak')!.textContent = String(myEntry.streak); document.getElementById('stat-best-streak')!.textContent = String(myEntry.best_streak); } } // Fetch bot names for predictor IDs const botNames = await fetchBotNames(data.entries.map((e: PredictorStats) => e.predictor_id)); container.innerHTML = ` ${data.entries.map((entry: PredictorStats, idx: number) => { const total = entry.correct + entry.incorrect; const accuracy = total > 0 ? Math.round((entry.correct / total) * 100) : 0; const streakClass = entry.streak > 0 ? 'positive' : entry.streak < 0 ? 'negative' : 'neutral'; const botName = botNames.get(entry.predictor_id) || entry.predictor_id; const isYou = entry.predictor_id === predictorId; return ` `; }).join('')}
Rank Predictor Correct Incorrect Accuracy Streak
#${idx + 1} ${botName}${isYou ? ' (you)' : ''} ${entry.correct} ${entry.incorrect}
${accuracy}%
${entry.streak > 0 ? '+' : ''}${entry.streak}

Updated: ${new Date(data.updated_at).toLocaleString()}

`; } catch (err) { console.error('Failed to load predictions leaderboard:', err); container.innerHTML = '
Failed to load leaderboard
'; } } function escapeHtml(str: string): string { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } async function fetchBotNames(botIds: string[]): Promise> { const names = new Map(); const uniqueIds = [...new Set(botIds)]; await Promise.all(uniqueIds.map(async id => { try { const response = await fetch(`${PAGES_BASE}/data/bots/${id}.json`); if (response.ok) { const bot: BotProfile = await response.json(); names.set(id, bot.name); } } catch { // Ignore errors, will use ID as fallback } })); return names; }