@@ -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 "> < 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