MediaWiki:Common.js: Difference between revisions

Add Users by Country counter widget for Admin page
Improve Users by Country counter with better UI and empty state handling
Line 696: Line 696:
(function() {
(function() {
   'use strict';
   'use strict';
  // Only run on the User:Admin page
   if (mw.config.get('wgPageName') !== 'User:Admin') return;
   if (mw.config.get('wgPageName') !== 'User:Admin') return;


  // Language code to country/region mapping
   var langToCountry = {
   var langToCountry = {
     'en': '🇺🇸 English-speaking',
     'en': '🇺🇸 English-speaking', 'de': '🇩🇪 Germany / German-speaking',
    'de': '🇩🇪 Germany / German-speaking',
     'fr': '🇫🇷 France / French-speaking', 'es': '🇪🇸 Spain / Spanish-speaking',
     'fr': '🇫🇷 France / French-speaking',
     'pt': '🇵🇹 Portugal / Portuguese-speaking', 'it': '🇮🇹 Italy / Italian-speaking',
    'es': '🇪🇸 Spain / Spanish-speaking',
     'nl': '🇳🇱 Netherlands / Dutch-speaking', 'pl': '🇵🇱 Poland / Polish-speaking',
     'pt': '🇵🇹 Portugal / Portuguese-speaking',
     'ru': '🇷🇺 Russia / Russian-speaking', 'ja': '🇯🇵 Japan',
    'it': '🇮🇹 Italy / Italian-speaking',
     'zh': '🇨🇳 China / Chinese-speaking', 'ar': '🇸🇦 Arabic-speaking',
     'nl': '🇳🇱 Netherlands / Dutch-speaking',
     'tr': '🇹🇷 Turkey / Turkish-speaking', 'sv': '🇸🇪 Sweden / Swedish-speaking',
    'pl': '🇵🇱 Poland / Polish-speaking',
     'da': '🇩🇰 Denmark / Danish-speaking', 'fi': '🇫🇮 Finland / Finnish-speaking',
     'ru': '🇷🇺 Russia / Russian-speaking',
     'nb': '🇳🇴 Norway / Norwegian-speaking', 'cs': '🇨🇿 Czech Republic',
    'ja': '🇯🇵 Japan',
     'hu': '🇭🇺 Hungary', 'ro': '🇷🇴 Romania', 'el': '🇬🇷 Greece / Greek-speaking',
     'zh': '🇨🇳 China / Chinese-speaking',
     'ko': '🇰🇷 Korea / Korean-speaking', 'uk': '🇺🇦 Ukraine / Ukrainian-speaking',
    'ar': '🇸🇦 Arabic-speaking',
     'he': '🇮🇱 Israel / Hebrew-speaking', 'id': '🇮🇩 Indonesia',
     'tr': '🇹🇷 Turkey / Turkish-speaking',
     'vi': '🇻🇳 Vietnam', 'th': '🇹🇭 Thailand', 'hi': '🇮🇳 India / Hindi-speaking',
    'sv': '🇸🇪 Sweden / Swedish-speaking',
     'bn': '🇧🇩 Bangladesh / Bengali-speaking', 'fa': '🇮🇷 Iran / Persian-speaking'
     'da': '🇩🇰 Denmark / Danish-speaking',
    'fi': '🇫🇮 Finland / Finnish-speaking',
     'nb': '🇳🇴 Norway / Norwegian-speaking',
    'cs': '🇨🇿 Czech Republic',
     'hu': '🇭🇺 Hungary',
    'ro': '🇷🇴 Romania',
    'el': '🇬🇷 Greece / Greek-speaking',
     'ko': '🇰🇷 Korea / Korean-speaking',
    'uk': '🇺🇦 Ukraine / Ukrainian-speaking',
     'he': '🇮🇱 Israel / Hebrew-speaking',
    'id': '🇮🇩 Indonesia',
     'vi': '🇻🇳 Vietnam',
    'th': '🇹🇭 Thailand',
    'hi': '🇮🇳 India / Hindi-speaking',
     'bn': '🇧🇩 Bangladesh / Bengali-speaking',
    'fa': '🇮🇷 Iran / Persian-speaking'
   };
   };


   function buildCounter() {
   function renderResults(langCounts, totalUsers) {
     var container = document.getElementById('country-user-counter');
     var container = document.getElementById('country-user-counter');
     if (!container) return;
     if (!container) return;


     container.innerHTML = '<p style="color:#aaa; font-style:italic;">⏳ Fetching user data...</p>';
     var sorted = Object.keys(langCounts)
      .filter(function(l) { return langCounts[l] > 0; })
      .sort(function(a, b) { return langCounts[b] - langCounts[a]; });
 
    var html = '<style>';
    html += '#cuc-wrap{font-family:sans-serif}';
    html += '#cuc-stats{display:flex;gap:20px;margin-bottom:14px;flex-wrap:wrap}';
    html += '.cuc-stat-card{background:#222;border:1px solid #444;border-radius:6px;padding:10px 18px;min-width:120px;text-align:center}';
    html += '.cuc-stat-num{font-size:1.8em;font-weight:bold;color:#e07000}';
    html += '.cuc-stat-lbl{font-size:0.8em;color:#aaa;margin-top:2px}';
    html += '#cuc-table{width:100%;border-collapse:collapse;margin-top:4px}';
    html += '#cuc-table th{background:#2a2a2a;color:#e07000;padding:8px 12px;text-align:left;border:1px solid #444;font-size:0.9em}';
    html += '#cuc-table td{padding:7px 12px;border:1px solid #2e2e2e;color:#ddd;font-size:0.9em}';
    html += '#cuc-table tr:nth-child(even) td{background:#1c1c1c}';
    html += '#cuc-table tr:hover td{background:#252525}';
    html += '.cuc-bar-wrap{background:#111;border-radius:3px;height:14px;overflow:hidden}';
    html += '.cuc-bar{height:14px;background:linear-gradient(90deg,#e07000,#ff9a00);border-radius:3px;transition:width 0.4s ease}';
    html += '.cuc-no-data{background:#1a1a1a;border:1px dashed #444;border-radius:6px;padding:16px;color:#aaa;line-height:1.6}';
    html += '.cuc-no-data code{background:#222;padding:2px 6px;border-radius:3px;color:#e07000;font-size:0.9em}';
    html += '#cuc-timestamp{color:#555;font-size:0.8em;margin-top:10px}';
    html += '</style>';


     // Step 1: Get all users
     html += '<div id="cuc-wrap">';
    var allUsers = [];


     function fetchUsers(aufrom) {
     // Stats cards
      var params = {
    html += '<div id="cuc-stats">';
        action: 'query',
    html += '<div class="cuc-stat-card"><div class="cuc-stat-num">' + totalUsers + '</div><div class="cuc-stat-lbl">Total Users</div></div>';
        list: 'allusers',
    html += '<div class="cuc-stat-card"><div class="cuc-stat-num">' + sorted.length + '</div><div class="cuc-stat-lbl">Countries / Regions</div></div>';
        aulimit: 500,
    var locatedUsers = sorted.reduce(function(sum, l) { return sum + langCounts[l]; }, 0);
        format: 'json'
    html += '<div class="cuc-stat-card"><div class="cuc-stat-num">' + locatedUsers + '</div><div class="cuc-stat-lbl">Users with Location</div></div>';
      };
    html += '</div>';
      if (aufrom) params.aufrom = aufrom;


       return $.getJSON(mw.util.wikiScript('api'), params).then(function(data) {
    if (sorted.length === 0) {
         var users = (data.query && data.query.allusers) || [];
       html += '<div class="cuc-no-data">';
         allUsers = allUsers.concat(users);
      html += '<strong>📍 No country data available yet.</strong><br>';
         if (data.continue && data.continue.aufrom) {
      html += 'This counter tracks users by country using the <strong>Babel extension</strong> language categories.<br>';
          return fetchUsers(data.continue.aufrom);
      html += 'Users can declare their language/country by adding babel tags to their user page:<br><br>';
         }
      html += '<code>{{#babel:en|de|fr}}</code><br><br>';
      html += 'Once users add babel tags, this counter will automatically display their country distribution.<br>';
      html += 'Currently tracking <strong>' + totalUsers + ' registered users</strong> on this wiki.';
      html += '</div>';
    } else {
      var maxCount = langCounts[sorted[0]] || 1;
      html += '<table id="cuc-table"><thead><tr>';
      html += '<th style="width:40px">#</th>';
      html += '<th>Country / Region</th>';
      html += '<th style="width:70px;text-align:center">Users</th>';
      html += '<th style="width:200px">Distribution</th>';
      html += '</tr></thead><tbody>';
      sorted.forEach(function(lang, idx) {
         var count = langCounts[lang];
        var label = langToCountry[lang] || ('🌐 ' + lang.toUpperCase());
         var pct = Math.round((count / locatedUsers) * 100);
         var barWidth = Math.round((count / maxCount) * 180);
        html += '<tr>';
        html += '<td style="color:#666;text-align:center">' + (idx + 1) + '</td>';
        html += '<td>' + label + '</td>';
        html += '<td style="text-align:center;font-weight:bold;color:#e07000">' + count + '</td>';
        html += '<td><div class="cuc-bar-wrap"><div class="cuc-bar" style="width:' + barWidth + 'px"></div></div></td>';
         html += '</tr>';
       });
       });
      html += '</tbody></table>';
     }
     }


     // Step 2: For each user, get their language preference
     html += '<div id="cuc-timestamp">Last updated: ' + new Date().toLocaleString() + '</div>';
    function fetchUserLanguages() {
    html += '</div>';
      var langCounts = {};
      var totalFetched = 0;
      var batchSize = 50;
      var userNames = allUsers.map(function(u) { return u.name; });
      var batches = [];


      for (var i = 0; i < userNames.length; i += batchSize) {
    container.innerHTML = html;
        batches.push(userNames.slice(i, i + batchSize));
  }
      }


      function processBatch(idx) {
  function buildCounter() {
        if (idx >= batches.length) {
    var container = document.getElementById('country-user-counter');
          renderResults(langCounts, allUsers.length);
    if (!container) return;
          return;
    container.innerHTML = '<p style="color:#aaa;font-style:italic">⏳ Loading user statistics by country...</p>';
        }
        var batch = batches[idx];
        return $.getJSON(mw.util.wikiScript('api'), {
          action: 'query',
          list: 'users',
          ususers: batch.join('|'),
          usprop: 'groups',
          format: 'json'
        }).then(function() {
          // Language isn't directly available via users list
          // Use userinfo for current user only, so we fallback to babel categories
          processBatch(idx + 1);
        });
      }
      processBatch(0);
    }


     // Step 3: Get language data from Babel extension categories
     var allUsers = [];
     function fetchBabelData() {
     var langCounts = {};
      var langCounts = {};
    var langs = Object.keys(langToCountry);
      var promises = [];


       // Fetch members of language categories (e.g., Category:User_en, Category:User_de, etc.)
    function fetchUsers(aufrom) {
       var langs = Object.keys(langToCountry);
       var params = { action: 'query', list: 'allusers', aulimit: 500, format: 'json' };
 
       if (aufrom) params.aufrom = aufrom;
       function fetchLangCategory(lang, offset) {
       return $.getJSON(mw.util.wikiScript('api'), params).then(function(data) {
        return $.getJSON(mw.util.wikiScript('api'), {
        var users = (data.query && data.query.allusers) || [];
          action: 'query',
        allUsers = allUsers.concat(users);
          list: 'categorymembers',
        if (data['continue'] && data['continue'].aufrom) return fetchUsers(data['continue'].aufrom);
          cmtitle: 'Category:User_' + lang,
          cmtype: 'page',
          cmnamespace: 2,
          cmlimit: 500,
          cmcontinue: offset || undefined,
          format: 'json'
        }).then(function(data) {
          var members = (data.query && data.query.categorymembers) || [];
          if (!langCounts[lang]) langCounts[lang] = 0;
          langCounts[lang] += members.length;
          if (data.continue && data.continue.cmcontinue) {
            return fetchLangCategory(lang, data.continue.cmcontinue);
          }
        }).catch(function() {
          // Category may not exist, ignore
        });
      }
 
      var chain = $.Deferred().resolve();
      langs.forEach(function(lang) {
        chain = chain.then(function() {
          return fetchLangCategory(lang);
        });
      });
 
      return chain.then(function() {
        return langCounts;
       });
       });
     }
     }


    // Render the results table
     function fetchLangCat(lang) {
     function renderResults(langCounts, totalUsers) {
       return $.getJSON(mw.util.wikiScript('api'), {
       var sorted = Object.keys(langCounts)
         action: 'query', list: 'categorymembers',
        .filter(function(l) { return langCounts[l] > 0; })
        cmtitle: 'Category:User_' + lang,
         .sort(function(a, b) { return langCounts[b] - langCounts[a]; });
        cmtype: 'page', cmnamespace: 2, cmlimit: 500, format: 'json'
 
       }).then(function(data) {
      var html = '<style>';
         var m = (data.query && data.query.categorymembers) || [];
      html += '#country-counter-table { width:100%; border-collapse:collapse; margin-top:10px; }';
         if (m.length > 0) langCounts[lang] = (langCounts[lang] || 0) + m.length;
      html += '#country-counter-table th { background:#2a2a2a; color:#e07000; padding:8px 12px; text-align:left; border:1px solid #444; }';
      }).catch(function() {});
      html += '#country-counter-table td { padding:7px 12px; border:1px solid #333; color:#ddd; }';
      html += '#country-counter-table tr:nth-child(even) td { background:#1a1a1a; }';
      html += '#country-counter-table tr:hover td { background:#252525; }';
      html += '.bar-cell { width:40%; }';
      html += '.bar { height:14px; background:#e07000; border-radius:3px; display:inline-block; min-width:2px; }';
      html += '</style>';
 
      html += '<p style="color:#ccc; margin-bottom:8px;">📊 <strong>Total registered users:</strong> ' + totalUsers + '</p>';
 
       if (sorted.length === 0) {
         html += '<p style="color:#aaa;">No Babel language categories found. Users can add their language to their profile using <code>{{#babel:en}}</code> on their user page.</p>';
        html += '<p style="color:#888; font-size:0.9em;">💡 Tip: To enable country tracking, ask users to add babel tags to their user pages.</p>';
      } else {
        var maxCount = langCounts[sorted[0]] || 1;
         html += '<table id="country-counter-table"><thead><tr><th>#</th><th>Country / Region</th><th>Users</th><th class="bar-cell">Distribution</th></tr></thead><tbody>';
        sorted.forEach(function(lang, idx) {
          var count = langCounts[lang];
          var label = langToCountry[lang] || lang.toUpperCase();
          var barWidth = Math.round((count / maxCount) * 200);
          html += '<tr>';
          html += '<td>' + (idx + 1) + '</td>';
          html += '<td>' + label + '</td>';
          html += '<td style="text-align:center; font-weight:bold;">' + count + '</td>';
          html += '<td class="bar-cell"><span class="bar" style="width:' + barWidth + 'px"></span></td>';
          html += '</tr>';
        });
        html += '</tbody></table>';
      }
 
      html += '<p style="color:#666; font-size:0.85em; margin-top:10px;">ℹ️ Based on Babel language categories. Last loaded: ' + new Date().toLocaleString() + '</p>';
 
      container.innerHTML = html;
     }
     }


    // Main flow: fetch users, then babel categories
     fetchUsers().then(function() {
     fetchUsers().then(function() {
       return fetchBabelData();
       var d = $.Deferred().resolve().promise();
     }).then(function(langCounts) {
      langs.forEach(function(lang) { d = d.then(function() { return fetchLangCat(lang); }); });
      return d;
     }).then(function() {
       renderResults(langCounts, allUsers.length);
       renderResults(langCounts, allUsers.length);
     }).catch(function(err) {
     }).catch(function(e) {
       container.innerHTML = '<p style="color:#f66;">Error loading data: ' + (err && err.message ? err.message : String(err)) + '</p>';
       container.innerHTML = '<p style="color:#f66">⚠️ Error loading data: ' + String(e) + '</p>';
     });
     });
   }
   }


  // Run after DOM is ready
   $(document).ready(function() { buildCounter(); });
   $(document).ready(function() {
    buildCounter();
  });
 
})();
})();
// =====================================================
// =====================================================
// End: Users by Country Counter
// End: Users by Country Counter
// =====================================================
// =====================================================