@@ -234,14 +234,42 @@ document.addEventListener("DOMContentLoaded", function () {
234234 refreshButton . innerHTML =
235235 '<i class="spinner-border spinner-border-sm me-2"></i>Refreshing...' ;
236236
237+ const featuredContainer = document . querySelector ( ".featured-towers" ) ;
238+ const allTowersContainer = document . getElementById ( "all-towers" ) ;
239+
240+ const loadingBanner = document . createElement ( "div" ) ;
241+ loadingBanner . className = "col-12 mb-3" ;
242+ const loadingId = `loading-${ Date . now ( ) } ` ;
243+ loadingBanner . innerHTML = `
244+ <div id="${ loadingId } " class="card h-100 bg-dark bg-gradient text-white">
245+ <div class="card-body d-flex align-items-center justify-content-between gap-3">
246+ <div class="d-flex align-items-center gap-2">
247+ <div class="spinner-border spinner-border-sm text-info" role="status" aria-hidden="true"></div>
248+ <div>
249+ <div class="fw-bold">Commander, get me the towers before I (the) maul you...</div>
250+ <div class="small text-muted">
251+ <span data-loading-status>Starting…</span>
252+ </div>
253+ </div>
254+ </div>
255+
256+ <div class="text-end">
257+ <div class="fw-bold"><span data-loading-count>0</span>/<span data-loading-total>?</span></div>
258+ <div class="progress mt-1" style="width: 180px; height: 8px;">
259+ <div class="progress-bar bg-info" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
260+ </div>
261+ </div>
262+ </div>
263+ </div>
264+ ` ;
265+
237266 try {
238267 window . originalCardsOrder = [ ] ;
239268
240269 if ( ! forceRefresh ) {
241270 const cachedData = localStorage . getItem ( "towerDataCache" ) ;
242271 const cacheTimestamp = localStorage . getItem ( "towerDataTimestamp" ) ;
243272
244- // use cache if it exists
245273 if ( cachedData && cacheTimestamp ) {
246274 const cacheAge = Date . now ( ) - parseInt ( cacheTimestamp ) ;
247275 const cacheMaxAge = 12 * 60 * 60 * 1000 ; // 12 hours (modify the first number only)
@@ -261,22 +289,191 @@ document.addEventListener("DOMContentLoaded", function () {
261289 }
262290 }
263291
264- document . getElementById ( "featured-towers" ) . innerHTML =
265- '<div class="col-12 text-center text-light p-5"><div class="spinner-border" role="status"></div><p class="mt-2">Commander get the highlights before I sell you to Lord Exo.</p></div>' ;
292+ featuredContainer . innerHTML = "" ;
293+ allTowersContainer . innerHTML = "" ;
294+
295+ featuredContainer . innerHTML =
296+ '<div class="col-12 text-center text-light p-3 small text-muted">Loading highlights…</div>' ;
297+
298+ allTowersContainer . appendChild ( loadingBanner ) ;
299+
300+ const bannerEl = document . getElementById ( loadingId ) ;
301+ const statusEl = bannerEl ?. querySelector ( "[data-loading-status]" ) ;
302+ const countEl = bannerEl ?. querySelector ( "[data-loading-count]" ) ;
303+ const totalEl = bannerEl ?. querySelector ( "[data-loading-total]" ) ;
304+ const barEl = bannerEl ?. querySelector ( ".progress-bar" ) ;
305+
306+ const setProgress = ( loaded , total , statusText ) => {
307+ if ( typeof statusText === "string" && statusEl )
308+ statusEl . textContent = statusText ;
309+ if ( countEl ) countEl . textContent = String ( loaded ) ;
310+ if ( totalEl ) totalEl . textContent = total > 0 ? String ( total ) : "?" ;
311+ if ( barEl ) {
312+ const pct =
313+ total > 0 ? Math . min ( 100 , Math . round ( ( loaded / total ) * 100 ) ) : 0 ;
314+ barEl . style . width = `${ pct } %` ;
315+ barEl . setAttribute ( "aria-valuenow" , String ( pct ) ) ;
316+ }
317+ } ;
318+
319+ const data = await wikiFetcher . fetchFromApi ( {
320+ page : wikiFetcher . sourcePage ,
321+ prop : "text" ,
322+ } ) ;
323+ if ( data . error ) throw new Error ( data . error . info ) ;
324+
325+ const htmlContent = data . parse ?. text ?. [ "*" ] ;
326+ if ( ! htmlContent ) throw new Error ( "No source page content" ) ;
327+
328+ const parser = new DOMParser ( ) ;
329+ const doc = parser . parseFromString (
330+ `<body>${ htmlContent } </body>` ,
331+ "text/html" ,
332+ ) ;
333+ const towerElements = doc . querySelectorAll ( ".CategoryTreeItem" ) ;
334+
335+ const baseTowers = Array . from ( towerElements )
336+ . filter ( ( element ) =>
337+ element . querySelector ( "a" ) ?. href . includes ( "User_blog:" ) ,
338+ )
339+ . map ( ( element ) => {
340+ const link = element . querySelector ( "a" ) ;
341+ let fullText = link ?. textContent ?. trim ( ) || "Unknown Tower" ;
342+
343+ if ( fullText . startsWith ( "User blog:" ) )
344+ fullText = fullText . replace ( "User blog:" , "" ) ;
345+
346+ const towerName = fullText . includes ( "/" )
347+ ? fullText . split ( "/" ) . pop ( )
348+ : fullText ;
349+ const href = link ?. getAttribute ( "href" ) || "" ;
350+ const pageTitle = href . replace ( / ^ \/ w i k i \/ / , "" ) || fullText ;
351+
352+ return {
353+ name : towerName ,
354+ pageTitle : decodeURIComponent ( pageTitle ) ,
355+ url : href ,
356+ image :
357+ "https://static.wikia.nocookie.net/tower-defense-sim/images/4/4a/Site-favicon.ico" ,
358+ author : fullText . includes ( "/" )
359+ ? fullText . split ( "/" ) [ 0 ]
360+ : "Wiki Contributor" ,
361+ featured : wikiFetcher . featuredTowers . includes ( fullText ) ,
362+ highlighted : window . highlights
363+ ? window . highlights . includes ( fullText )
364+ : false ,
365+ verified : window . approvedTowers
366+ ? window . approvedTowers . includes ( fullText )
367+ : false ,
368+ unverified : window . approvedTowers
369+ ? ! window . approvedTowers . includes ( fullText )
370+ : true ,
371+ grandfathered : window . grandfatheredTowers
372+ ? window . grandfatheredTowers . includes ( fullText )
373+ : false ,
374+ uploadDate : "Recently" ,
375+ } ;
376+ } ) ;
377+
378+ const total = baseTowers . length ;
379+ setProgress ( 0 , total , "Fetching tower pages…" ) ;
380+
381+ featuredContainer . innerHTML = "" ;
382+ const anyHighlightsExpected = baseTowers . some ( ( t ) => t . highlighted ) ;
383+ if ( ! anyHighlightsExpected ) {
384+ featuredContainer . innerHTML =
385+ '<div class="col-12 text-center text-light p-3">No highlights at this time.</div>' ;
386+ }
387+
388+ const concurrency = 5 ;
389+ const streamedTowers = [ ] ;
390+ const renderedFeaturedNames = new Set ( ) ;
391+ const renderedMainNames = new Set ( ) ;
392+ let completed = 0 ;
266393
267- document . getElementById ( "all-towers" ) . innerHTML =
268- '<div class="col-12 text-center text-light p-5"><div class="spinner-border" role="status"></div><p class="mt-2">Loading towers from the TDS Wiki...</p></div>' ;
394+ const onTowerReady = ( tower ) => {
395+ streamedTowers . push ( tower ) ;
269396
270- const towers = await wikiFetcher . fetchTowers ( forceRefresh ) ;
397+ // featured/highlighted render
398+ if (
399+ tower . highlighted &&
400+ ! renderedFeaturedNames . has ( tower . pageTitle || tower . name )
401+ ) {
402+ renderTowerCard ( tower , featuredContainer ) ;
403+ renderedFeaturedNames . add ( tower . pageTitle || tower . name ) ;
404+ }
405+
406+ // main list render
407+ const mainKey = tower . pageTitle || tower . name ;
408+ if ( ! renderedMainNames . has ( mainKey ) ) {
409+ renderTowerCard ( tower , allTowersContainer ) ;
410+ renderedMainNames . add ( mainKey ) ;
411+ }
412+ } ;
413+
414+ let nextIndex = 0 ;
415+
416+ const worker = async ( ) => {
417+ while ( true ) {
418+ const i = nextIndex ++ ;
419+ if ( i >= baseTowers . length ) return ;
420+
421+ const tower = baseTowers [ i ] ;
422+ setProgress ( completed , total , `Fetching… ${ tower . name } ` ) ;
423+
424+ try {
425+ await wikiFetcher . enrichTowerData ( tower ) ;
426+ } catch ( e ) {
427+ console . log ( `Failed to fetch tower ${ tower . name } :` , e ) ;
428+ }
429+
430+ completed ++ ;
431+ onTowerReady ( tower ) ;
432+ setProgress ( completed , total , `Loaded ${ completed } /${ total } ` ) ;
271433
272- localStorage . setItem ( "towerDataCache" , JSON . stringify ( towers ) ) ;
434+ // Yield so UI repaints promptly (especially on slower machines)
435+ await new Promise ( ( r ) => setTimeout ( r , 0 ) ) ;
436+ }
437+ } ;
438+
439+ const workers = [ ] ;
440+ for ( let w = 0 ; w < Math . min ( concurrency , baseTowers . length ) ; w ++ ) {
441+ workers . push ( worker ( ) ) ;
442+ }
443+
444+ await Promise . allSettled ( workers ) ;
445+
446+ localStorage . setItem ( "towerDataCache" , JSON . stringify ( streamedTowers ) ) ;
273447 localStorage . setItem ( "towerDataTimestamp" , Date . now ( ) . toString ( ) ) ;
274448
275- renderAllTowers ( towers ) ;
449+ if ( window . setupSearch ) window . setupSearch ( ) ;
450+ if ( window . applyFilters ) window . applyFilters ( ) ;
451+ if ( window . setupSorting ) window . setupSorting ( ) ;
452+
453+ setProgress ( total , total , "Done" ) ;
454+ if ( bannerEl ) {
455+ bannerEl . classList . add ( "border" , "border-success" ) ;
456+ const spinner = bannerEl . querySelector ( ".spinner-border" ) ;
457+ if ( spinner ) spinner . remove ( ) ;
458+ }
459+
460+ setTimeout ( ( ) => {
461+ const el = document . getElementById ( loadingId ) ;
462+ if ( el && el . parentNode ) {
463+ el . parentNode . removeChild ( el ) ;
464+ }
465+ } , 1200 ) ;
276466 } catch ( error ) {
277467 console . error ( "Failed to load towers:" , error ) ;
278- document . getElementById ( "all-towers" ) . innerHTML =
279- '<div class="col-12 text-center text-danger p-5"><i class="bi bi-exclamation-triangle-fill fs-1"></i><p class="mt-2">Failed to load towers from the wiki. Please try again later.</p></div>' ;
468+
469+ allTowersContainer . innerHTML = `
470+ <div class="col-12">
471+ <div class="alert alert-danger">
472+ <div class="fw-bold"><i class="bi bi-exclamation-triangle-fill me-2"></i>Failed to load towers from the wiki.</div>
473+ <div class="small">Please try again later.</div>
474+ </div>
475+ </div>
476+ ` ;
280477 } finally {
281478 refreshButton . disabled = false ;
282479 refreshButton . innerHTML =
0 commit comments