Create script.js
Browse files- static/script.js +149 -0
static/script.js
ADDED
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
document.addEventListener('DOMContentLoaded', () => {
|
2 |
+
const leaderboardBody = document.getElementById('leaderboard-body');
|
3 |
+
const loadingIndicator = document.getElementById('loading-indicator');
|
4 |
+
const lastUpdatedElement = document.getElementById('last-updated');
|
5 |
+
const modal = document.getElementById('code-modal');
|
6 |
+
const modalUsername = document.getElementById('modal-username');
|
7 |
+
const modalCode = document.getElementById('modal-code');
|
8 |
+
const closeModalButton = document.querySelector('.close-button');
|
9 |
+
|
10 |
+
// Fetch from the FastAPI backend endpoint
|
11 |
+
const API_URL = '/api/leaderboard';
|
12 |
+
const REFRESH_INTERVAL_MS = 60 * 1000; // 60 seconds
|
13 |
+
|
14 |
+
async function fetchData() {
|
15 |
+
try {
|
16 |
+
// Fetch directly from our backend API
|
17 |
+
const response = await fetch(API_URL);
|
18 |
+
if (!response.ok) {
|
19 |
+
// Try to get error message from backend if available
|
20 |
+
let errorMsg = `HTTP error! status: ${response.status}`;
|
21 |
+
try {
|
22 |
+
const errorData = await response.json();
|
23 |
+
errorMsg = errorData.detail || errorMsg;
|
24 |
+
} catch (e) { /* Ignore if response is not JSON */ }
|
25 |
+
throw new Error(errorMsg);
|
26 |
+
}
|
27 |
+
const data = await response.json();
|
28 |
+
// The data is expected to be the already sorted list of objects
|
29 |
+
return data;
|
30 |
+
} catch (error) {
|
31 |
+
console.error("Failed to fetch leaderboard data:", error);
|
32 |
+
lastUpdatedElement.textContent = `Error: ${error.message}`; // Display error
|
33 |
+
return null; // Return null to indicate failure
|
34 |
+
}
|
35 |
+
}
|
36 |
+
|
37 |
+
// No sorting needed here - backend does it
|
38 |
+
// function sortData(data) { ... } // REMOVED
|
39 |
+
|
40 |
+
function formatTimestamp(isoString) {
|
41 |
+
// Keep this helper function
|
42 |
+
try {
|
43 |
+
const date = new Date(isoString);
|
44 |
+
return date.toISOString().slice(0, 19).replace('T', ' ');
|
45 |
+
} catch (e) {
|
46 |
+
return 'Invalid Date';
|
47 |
+
}
|
48 |
+
}
|
49 |
+
|
50 |
+
function renderLeaderboard(leaderboardData) {
|
51 |
+
// Clear previous content
|
52 |
+
leaderboardBody.innerHTML = '';
|
53 |
+
|
54 |
+
if (!leaderboardData || leaderboardData.length === 0) {
|
55 |
+
// Check if the loading indicator is still displayed, means fetch failed on load
|
56 |
+
if (loadingIndicator.style.display !== 'none') {
|
57 |
+
leaderboardBody.innerHTML = '<div class="leaderboard-row" style="justify-content: center; color: #aaa;">No data available or failed to load. Check logs.</div>';
|
58 |
+
} else {
|
59 |
+
// If fetch failed during refresh, keep old data and show error in footer? Or clear?
|
60 |
+
// Let's clear for now and rely on error message in footer.
|
61 |
+
leaderboardBody.innerHTML = '<div class="leaderboard-row" style="justify-content: center; color: #aaa;">Failed to refresh data.</div>';
|
62 |
+
}
|
63 |
+
return;
|
64 |
+
}
|
65 |
+
|
66 |
+
leaderboardData.forEach((entry, index) => {
|
67 |
+
const rank = index + 1;
|
68 |
+
const row = document.createElement('div');
|
69 |
+
row.className = 'leaderboard-row';
|
70 |
+
row.setAttribute('data-rank', rank);
|
71 |
+
|
72 |
+
row.innerHTML = `
|
73 |
+
<div class="rank">#${rank}</div>
|
74 |
+
<div class="username" title="${entry.username}">${entry.username}</div>
|
75 |
+
<div class="score">${entry.score} pts</div>
|
76 |
+
<div class="timestamp">${formatTimestamp(entry.timestamp)}</div>
|
77 |
+
<div class="code-action">
|
78 |
+
<button class="view-code-btn" data-username="${entry.username}">View</button>
|
79 |
+
</div>
|
80 |
+
`;
|
81 |
+
|
82 |
+
const button = row.querySelector('.view-code-btn');
|
83 |
+
button._codeData = entry.code; // Store code directly
|
84 |
+
button.addEventListener('click', handleViewCodeClick);
|
85 |
+
|
86 |
+
leaderboardBody.appendChild(row);
|
87 |
+
});
|
88 |
+
}
|
89 |
+
|
90 |
+
function handleViewCodeClick(event) {
|
91 |
+
const button = event.currentTarget;
|
92 |
+
const username = button.getAttribute('data-username');
|
93 |
+
const code = button._codeData;
|
94 |
+
showCodeModal(username, code);
|
95 |
+
}
|
96 |
+
|
97 |
+
function showCodeModal(username, code) {
|
98 |
+
modalUsername.textContent = `Code from ${username}`;
|
99 |
+
modalCode.textContent = code || '// No code submitted or available';
|
100 |
+
|
101 |
+
if (typeof hljs !== 'undefined') {
|
102 |
+
modalCode.className = 'language-python';
|
103 |
+
hljs.highlightElement(modalCode);
|
104 |
+
}
|
105 |
+
modal.style.display = 'block';
|
106 |
+
}
|
107 |
+
|
108 |
+
function hideCodeModal() {
|
109 |
+
modal.style.display = 'none';
|
110 |
+
modalCode.textContent = '';
|
111 |
+
modalUsername.textContent = '';
|
112 |
+
}
|
113 |
+
|
114 |
+
async function updateLeaderboard() {
|
115 |
+
if (!leaderboardBody.hasChildNodes()) {
|
116 |
+
loadingIndicator.style.display = 'flex';
|
117 |
+
} else {
|
118 |
+
lastUpdatedElement.textContent = 'Updating...';
|
119 |
+
}
|
120 |
+
|
121 |
+
const leaderboardData = await fetchData(); // Fetches already sorted data
|
122 |
+
|
123 |
+
loadingIndicator.style.display = 'none'; // Hide loading indicator regardless of success/fail
|
124 |
+
|
125 |
+
if (leaderboardData) {
|
126 |
+
renderLeaderboard(leaderboardData);
|
127 |
+
// Only update timestamp on success
|
128 |
+
lastUpdatedElement.textContent = `Last updated: ${new Date().toLocaleTimeString()}`;
|
129 |
+
} else {
|
130 |
+
// Error occurred, message already set in fetchData
|
131 |
+
// Optionally render empty state if needed
|
132 |
+
if (!leaderboardBody.hasChildNodes()) {
|
133 |
+
renderLeaderboard(null); // Show 'No data' message if initial load failed
|
134 |
+
}
|
135 |
+
}
|
136 |
+
}
|
137 |
+
|
138 |
+
// --- Event Listeners ---
|
139 |
+
closeModalButton.addEventListener('click', hideCodeModal);
|
140 |
+
window.addEventListener('click', (event) => {
|
141 |
+
if (event.target === modal) {
|
142 |
+
hideCodeModal();
|
143 |
+
}
|
144 |
+
});
|
145 |
+
|
146 |
+
// --- Initial Load and Auto-Refresh ---
|
147 |
+
updateLeaderboard(); // Initial load
|
148 |
+
setInterval(updateLeaderboard, REFRESH_INTERVAL_MS); // Refresh every 60 seconds
|
149 |
+
});
|