@@ -147,10 +147,117 @@ <h1 class="page-title">Leaderboard</h1>
147147 </ div >
148148 </ div >
149149
150- < div id ="leaderboard-body "> </ div >
150+ < div id ="leaderboard-body ">
151+ <!-- Skeleton rows — shown on initial load, cleared by renderLeaderboard() -->
152+ < div class ="skeleton-row ">
153+ < div class ="skeleton-cell "> </ div >
154+ < div class ="skeleton-cell "> </ div >
155+ < div class ="skeleton-cell "> </ div >
156+ < div class ="skeleton-cell "> </ div >
157+ < div class ="skeleton-cell "> </ div >
158+ < div class ="skeleton-cell "> </ div >
159+ < div class ="skeleton-cell "> </ div >
160+ < div class ="skeleton-cell "> </ div >
161+ </ div >
162+ < div class ="skeleton-row ">
163+ < div class ="skeleton-cell "> </ div >
164+ < div class ="skeleton-cell "> </ div >
165+ < div class ="skeleton-cell "> </ div >
166+ < div class ="skeleton-cell "> </ div >
167+ < div class ="skeleton-cell "> </ div >
168+ < div class ="skeleton-cell "> </ div >
169+ < div class ="skeleton-cell "> </ div >
170+ < div class ="skeleton-cell "> </ div >
171+ </ div >
172+ < div class ="skeleton-row ">
173+ < div class ="skeleton-cell "> </ div >
174+ < div class ="skeleton-cell "> </ div >
175+ < div class ="skeleton-cell "> </ div >
176+ < div class ="skeleton-cell "> </ div >
177+ < div class ="skeleton-cell "> </ div >
178+ < div class ="skeleton-cell "> </ div >
179+ < div class ="skeleton-cell "> </ div >
180+ < div class ="skeleton-cell "> </ div >
181+ </ div >
182+ < div class ="skeleton-row ">
183+ < div class ="skeleton-cell "> </ div >
184+ < div class ="skeleton-cell "> </ div >
185+ < div class ="skeleton-cell "> </ div >
186+ < div class ="skeleton-cell "> </ div >
187+ < div class ="skeleton-cell "> </ div >
188+ < div class ="skeleton-cell "> </ div >
189+ < div class ="skeleton-cell "> </ div >
190+ < div class ="skeleton-cell "> </ div >
191+ </ div >
192+ < div class ="skeleton-row ">
193+ < div class ="skeleton-cell "> </ div >
194+ < div class ="skeleton-cell "> </ div >
195+ < div class ="skeleton-cell "> </ div >
196+ < div class ="skeleton-cell "> </ div >
197+ < div class ="skeleton-cell "> </ div >
198+ < div class ="skeleton-cell "> </ div >
199+ < div class ="skeleton-cell "> </ div >
200+ < div class ="skeleton-cell "> </ div >
201+ </ div >
202+ < div class ="skeleton-row ">
203+ < div class ="skeleton-cell "> </ div >
204+ < div class ="skeleton-cell "> </ div >
205+ < div class ="skeleton-cell "> </ div >
206+ < div class ="skeleton-cell "> </ div >
207+ < div class ="skeleton-cell "> </ div >
208+ < div class ="skeleton-cell "> </ div >
209+ < div class ="skeleton-cell "> </ div >
210+ < div class ="skeleton-cell "> </ div >
211+ </ div >
212+ < div class ="skeleton-row ">
213+ < div class ="skeleton-cell "> </ div >
214+ < div class ="skeleton-cell "> </ div >
215+ < div class ="skeleton-cell "> </ div >
216+ < div class ="skeleton-cell "> </ div >
217+ < div class ="skeleton-cell "> </ div >
218+ < div class ="skeleton-cell "> </ div >
219+ < div class ="skeleton-cell "> </ div >
220+ < div class ="skeleton-cell "> </ div >
221+ </ div >
222+ < div class ="skeleton-row ">
223+ < div class ="skeleton-cell "> </ div >
224+ < div class ="skeleton-cell "> </ div >
225+ < div class ="skeleton-cell "> </ div >
226+ < div class ="skeleton-cell "> </ div >
227+ < div class ="skeleton-cell "> </ div >
228+ < div class ="skeleton-cell "> </ div >
229+ < div class ="skeleton-cell "> </ div >
230+ < div class ="skeleton-cell "> </ div >
231+ </ div >
232+ </ div >
151233 </ div >
152234
153- < div class ="mobile-cards " id ="mobile-cards "> </ div >
235+ < div class ="mobile-cards " id ="mobile-cards ">
236+ <!-- Skeleton mobile cards — shown on initial load, cleared by renderLeaderboard() -->
237+ < div class ="skeleton-card "> </ div >
238+ < div class ="skeleton-card "> </ div >
239+ < div class ="skeleton-card "> </ div >
240+ < div class ="skeleton-card "> </ div >
241+ < div class ="skeleton-card "> </ div >
242+ < div class ="skeleton-card "> </ div >
243+ < div class ="skeleton-card "> </ div >
244+ < div class ="skeleton-card "> </ div >
245+ </ div >
246+
247+ <!-- Error State -->
248+ < div id ="leaderboard-error " class ="leaderboard-error ">
249+ < div class ="leaderboard-error-content ">
250+ < div class ="leaderboard-error-icon "> [!]</ div >
251+ < div class ="leaderboard-error-msg ">
252+ LEADERBOARD_DATA_UNAVAILABLE
253+ </ div >
254+ < div class ="leaderboard-error-desc ">
255+ Failed to fetch leaderboard data. The upstream API may be
256+ rate-limited or unreachable. Please try again.
257+ </ div >
258+ < button id ="retry-btn " class ="btn btn-error "> [RETRY]</ button >
259+ </ div >
260+ </ div >
154261
155262 < div id ="pagination-controls " class ="pagination-controls ">
156263 < button id ="prev-page-btn " class ="page-nav-btn "> < PREV</ button >
@@ -176,6 +283,15 @@ <h1 class="page-title">Leaderboard</h1>
176283 </ div >
177284
178285 < script nonce ="__NONCE__ ">
286+ // ── Error State Helpers ──
287+ function showError ( ) {
288+ document . getElementById ( "leaderboard-error" ) . classList . add ( "active" ) ;
289+ }
290+
291+ function hideError ( ) {
292+ document . getElementById ( "leaderboard-error" ) . classList . remove ( "active" ) ;
293+ }
294+
179295 document . addEventListener ( "DOMContentLoaded" , ( ) => {
180296 document . querySelectorAll ( ".tab" ) . forEach ( ( tab ) => {
181297 tab . addEventListener ( "click" , ( ) => {
@@ -185,9 +301,22 @@ <h1 class="page-title">Leaderboard</h1>
185301
186302 setupSearchListeners ( ) ;
187303 setupPaginationListeners ( ) ;
304+ // Skeleton rows are already in #leaderboard-body — shown until renderLeaderboard() clears them
188305 fetchLeaderboardData ( ) ;
189306 // Poll every 2 minutes to detect new syncs
190307 setInterval ( fetchLeaderboardData , 2 * 60 * 1000 ) ;
308+
309+ // Retry button — re-fetches data
310+ document
311+ . getElementById ( "retry-btn" )
312+ . addEventListener ( "click" , function ( ) {
313+ hideError ( ) ;
314+ leaderboardData [ "overall" ] = null ;
315+ leaderboardData [ "monthly" ] = null ;
316+ leaderboardData [ "weekly" ] = null ;
317+ leaderboardData [ "daily" ] = null ;
318+ fetchLeaderboardData ( ) ;
319+ } ) ;
191320 } ) ;
192321
193322 const leaderboardData = { } ;
@@ -211,14 +340,27 @@ <h1 class="page-title">Leaderboard</h1>
211340 ) ,
212341 ) ;
213342
343+ let anySuccess = false ;
214344 results . forEach ( ( result , index ) => {
215345 if ( result . status === "fulfilled" ) {
216346 leaderboardData [ endpoints [ index ] ] = result . value ;
347+ anySuccess = true ;
217348 } else {
218349 console . error ( "Failed to fetch" , endpoints [ index ] , result . reason ) ;
219350 }
220351 } ) ;
221352
353+ // If ALL endpoints failed, show error state instead of blank page
354+ if ( ! anySuccess ) {
355+ document . getElementById ( "leaderboard-body" ) . innerHTML = "" ;
356+ document . getElementById ( "mobile-cards" ) . innerHTML = "" ;
357+ document . getElementById ( "leaderboard-stats" ) . innerHTML = "" ;
358+ hideError ( ) ; // reset in case retry was clicked
359+ showError ( ) ;
360+ return ;
361+ }
362+ hideError ( ) ;
363+
222364 try {
223365 // Fetch directly from github to avoid needing a server redeploy for new data
224366 const syncRes = await fetch (
@@ -282,6 +424,16 @@ <h1 class="page-title">Leaderboard</h1>
282424
283425 const originalData = leaderboardData [ activeDatasetType ] ;
284426
427+ // Empty state — data exists but is empty array
428+ if ( originalData . length === 0 ) {
429+ document . getElementById ( "leaderboard-body" ) . innerHTML =
430+ '<div class="leaderboard-empty">[SYS]: NO_LEADERBOARD_DATA_YET</div>' ;
431+ document . getElementById ( "mobile-cards" ) . innerHTML =
432+ '<div class="leaderboard-empty">[SYS]: NO_LEADERBOARD_DATA_YET</div>' ;
433+ document . getElementById ( "leaderboard-stats" ) . innerHTML = "" ;
434+ return ;
435+ }
436+
285437 const filteredData = originalData . filter ( ( user ) => {
286438 if ( ! currentSearchTerm ) return true ;
287439
0 commit comments