Skip to content

Commit bb54ea3

Browse files
authored
feat: add loading skeleton, error states, and empty state to leaderboard UI (#208)
* feat: add loading skeleton, error states, and empty state to leaderboard UI Implement Issue #179: Loading Skeleton + Error States for Leaderboard. - Add 8 skeleton shimmer rows (desktop) and 8 skeleton cards (mobile) that display on page load, naturally cleared by renderLeaderboard() - Add error banner with [RETRY] button when all 4 data endpoints fail - Error re-fetches data on click, clears cached entries - Add empty state message when API returns no data - Add CSS shimmer animation, fade-in transition for data rows - All states responsive — skeleton rows hidden on mobile, skeleton cards hidden on desktop Resolves #179 * style: run prettier on frontend/styles/main.css * fix: replace corrupted non-ASCII chars with ASCII equivalents in CSS comments All 33 section header comments in main.css had Unicode box-drawing characters (U+2500, U+2014, U+2192, U+2794) that got corrupted during an earlier encoding conversion, rendering as '??????' in GitHub's diff. - ─ (U+2500) → -- - — (U+2014) → --- - → (U+2192) → -> - ➔ (U+2794) → > Rebuilt the file from the clean upstream base to avoid any lingering encoding corruption. All CSS content values using arrows were converted to their ASCII equivalents. Fixes review feedback on PR #208. * Update leaderboard.html
1 parent 7039764 commit bb54ea3

2 files changed

Lines changed: 341 additions & 37 deletions

File tree

frontend/leaderboard.html

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,10 +223,117 @@ <h1 class="page-title">Leaderboard</h1>
223223
</div>
224224
</div>
225225

226-
<div id="leaderboard-body"></div>
226+
<div id="leaderboard-body">
227+
<!-- Skeleton rows — shown on initial load, cleared by renderLeaderboard() -->
228+
<div class="skeleton-row">
229+
<div class="skeleton-cell"></div>
230+
<div class="skeleton-cell"></div>
231+
<div class="skeleton-cell"></div>
232+
<div class="skeleton-cell"></div>
233+
<div class="skeleton-cell"></div>
234+
<div class="skeleton-cell"></div>
235+
<div class="skeleton-cell"></div>
236+
<div class="skeleton-cell"></div>
237+
</div>
238+
<div class="skeleton-row">
239+
<div class="skeleton-cell"></div>
240+
<div class="skeleton-cell"></div>
241+
<div class="skeleton-cell"></div>
242+
<div class="skeleton-cell"></div>
243+
<div class="skeleton-cell"></div>
244+
<div class="skeleton-cell"></div>
245+
<div class="skeleton-cell"></div>
246+
<div class="skeleton-cell"></div>
247+
</div>
248+
<div class="skeleton-row">
249+
<div class="skeleton-cell"></div>
250+
<div class="skeleton-cell"></div>
251+
<div class="skeleton-cell"></div>
252+
<div class="skeleton-cell"></div>
253+
<div class="skeleton-cell"></div>
254+
<div class="skeleton-cell"></div>
255+
<div class="skeleton-cell"></div>
256+
<div class="skeleton-cell"></div>
257+
</div>
258+
<div class="skeleton-row">
259+
<div class="skeleton-cell"></div>
260+
<div class="skeleton-cell"></div>
261+
<div class="skeleton-cell"></div>
262+
<div class="skeleton-cell"></div>
263+
<div class="skeleton-cell"></div>
264+
<div class="skeleton-cell"></div>
265+
<div class="skeleton-cell"></div>
266+
<div class="skeleton-cell"></div>
267+
</div>
268+
<div class="skeleton-row">
269+
<div class="skeleton-cell"></div>
270+
<div class="skeleton-cell"></div>
271+
<div class="skeleton-cell"></div>
272+
<div class="skeleton-cell"></div>
273+
<div class="skeleton-cell"></div>
274+
<div class="skeleton-cell"></div>
275+
<div class="skeleton-cell"></div>
276+
<div class="skeleton-cell"></div>
277+
</div>
278+
<div class="skeleton-row">
279+
<div class="skeleton-cell"></div>
280+
<div class="skeleton-cell"></div>
281+
<div class="skeleton-cell"></div>
282+
<div class="skeleton-cell"></div>
283+
<div class="skeleton-cell"></div>
284+
<div class="skeleton-cell"></div>
285+
<div class="skeleton-cell"></div>
286+
<div class="skeleton-cell"></div>
287+
</div>
288+
<div class="skeleton-row">
289+
<div class="skeleton-cell"></div>
290+
<div class="skeleton-cell"></div>
291+
<div class="skeleton-cell"></div>
292+
<div class="skeleton-cell"></div>
293+
<div class="skeleton-cell"></div>
294+
<div class="skeleton-cell"></div>
295+
<div class="skeleton-cell"></div>
296+
<div class="skeleton-cell"></div>
297+
</div>
298+
<div class="skeleton-row">
299+
<div class="skeleton-cell"></div>
300+
<div class="skeleton-cell"></div>
301+
<div class="skeleton-cell"></div>
302+
<div class="skeleton-cell"></div>
303+
<div class="skeleton-cell"></div>
304+
<div class="skeleton-cell"></div>
305+
<div class="skeleton-cell"></div>
306+
<div class="skeleton-cell"></div>
307+
</div>
308+
</div>
227309
</div>
228310

229-
<div class="mobile-cards" id="mobile-cards"></div>
311+
<div class="mobile-cards" id="mobile-cards">
312+
<!-- Skeleton mobile cards — shown on initial load, cleared by renderLeaderboard() -->
313+
<div class="skeleton-card"></div>
314+
<div class="skeleton-card"></div>
315+
<div class="skeleton-card"></div>
316+
<div class="skeleton-card"></div>
317+
<div class="skeleton-card"></div>
318+
<div class="skeleton-card"></div>
319+
<div class="skeleton-card"></div>
320+
<div class="skeleton-card"></div>
321+
</div>
322+
323+
<!-- Error State -->
324+
<div id="leaderboard-error" class="leaderboard-error">
325+
<div class="leaderboard-error-content">
326+
<div class="leaderboard-error-icon">[!]</div>
327+
<div class="leaderboard-error-msg">
328+
LEADERBOARD_DATA_UNAVAILABLE
329+
</div>
330+
<div class="leaderboard-error-desc">
331+
Failed to fetch leaderboard data. The upstream API may be
332+
rate-limited or unreachable. Please try again.
333+
</div>
334+
<button id="retry-btn" class="btn btn-error">RETRY</button>
335+
</div>
336+
</div>
230337

231338
<div id="pagination-controls" class="pagination-controls">
232339
<button id="prev-page-btn" class="page-nav-btn">&lt; PREV</button>
@@ -252,6 +359,15 @@ <h1 class="page-title">Leaderboard</h1>
252359
</div>
253360

254361
<script nonce="__NONCE__">
362+
// ── Error State Helpers ──
363+
function showError() {
364+
document.getElementById("leaderboard-error").classList.add("active");
365+
}
366+
367+
function hideError() {
368+
document.getElementById("leaderboard-error").classList.remove("active");
369+
}
370+
255371
document.addEventListener("DOMContentLoaded", () => {
256372
document.querySelectorAll(".tab").forEach((tab) => {
257373
tab.addEventListener("click", () => {
@@ -261,6 +377,7 @@ <h1 class="page-title">Leaderboard</h1>
261377

262378
setupSearchListeners();
263379
setupPaginationListeners();
380+
// Skeleton rows are already in #leaderboard-body — shown until renderLeaderboard() clears them
264381
fetchLeaderboardData();
265382
// Poll every 2 minutes to detect new syncs
266383
// Page Visibility API — pause polling when tab is hidden
@@ -285,6 +402,18 @@ <h1 class="page-title">Leaderboard</h1>
285402
});
286403

287404
startPolling();
405+
406+
// Retry button — re-fetches data
407+
document
408+
.getElementById("retry-btn")
409+
.addEventListener("click", function () {
410+
hideError();
411+
leaderboardData["overall"] = null;
412+
leaderboardData["monthly"] = null;
413+
leaderboardData["weekly"] = null;
414+
leaderboardData["daily"] = null;
415+
fetchLeaderboardData();
416+
});
288417
});
289418

290419
window.leaderboardData = {};
@@ -308,14 +437,27 @@ <h1 class="page-title">Leaderboard</h1>
308437
),
309438
);
310439

440+
let anySuccess = false;
311441
results.forEach((result, index) => {
312442
if (result.status === "fulfilled") {
313443
window.leaderboardData[endpoints[index]] = result.value;
444+
anySuccess = true;
314445
} else {
315446
console.error("Failed to fetch", endpoints[index], result.reason);
316447
}
317448
});
318449

450+
// If ALL endpoints failed, show error state instead of blank page
451+
if (!anySuccess) {
452+
document.getElementById("leaderboard-body").innerHTML = "";
453+
document.getElementById("mobile-cards").innerHTML = "";
454+
document.getElementById("leaderboard-stats").innerHTML = "";
455+
hideError(); // reset in case retry was clicked
456+
showError();
457+
return;
458+
}
459+
hideError();
460+
319461
try {
320462
// Fetch directly from github to avoid needing a server redeploy for new data
321463
const syncRes = await fetch(
@@ -379,6 +521,16 @@ <h1 class="page-title">Leaderboard</h1>
379521

380522
const originalData = window.leaderboardData[activeDatasetType];
381523

524+
// Empty state — data exists but is empty array
525+
if (originalData.length === 0) {
526+
document.getElementById("leaderboard-body").innerHTML =
527+
'<div class="leaderboard-empty">[SYS]: NO_LEADERBOARD_DATA_YET</div>';
528+
document.getElementById("mobile-cards").innerHTML =
529+
'<div class="leaderboard-empty">[SYS]: NO_LEADERBOARD_DATA_YET</div>';
530+
document.getElementById("leaderboard-stats").innerHTML = "";
531+
return;
532+
}
533+
382534
const filteredData = originalData.filter((user) => {
383535
if (!currentSearchTerm) return true;
384536

0 commit comments

Comments
 (0)