Jump to content

MediaWiki:Common.js: Difference between revisions

From Utopia Game
No edit summary
No edit summary
Line 1: Line 1:
/* Any JavaScript here will be loaded for all users on every page load. */
/* Any JavaScript here will be loaded for all users on every page load. */


/* =========================================================
  Tabs (unchanged)
  ========================================================= */
(function () {
(function () {
  /* =========================================================
    CONFIG
    ========================================================= */
   const DISCORD_URL = 'https://discord.gg/t2Rp2dRvze';
   const DISCORD_URL = 'https://discord.gg/t2Rp2dRvze';


Line 11: Line 11:


   // Clock timezone (same for everyone)
   // Clock timezone (same for everyone)
   const CLOCK_TIMEZONE = 'Etc/UTC'; // change if desired
   const CLOCK_TIMEZONE = 'Etc/UTC';


   // Game time anchor (real UTC -> game time)
   // Game time anchor (real UTC -> game time)
Line 23: Line 23:
   const pad2 = (n) => String(n).padStart(2, '0');
   const pad2 = (n) => String(n).padStart(2, '0');


   function ensureDiscordLink(icons) {
  /* =========================================================
     if (document.getElementById('custom-sticky-link')) return;
    Tabs (your existing code)
    ========================================================= */
  $(document).ready(function () {
    $('.wiki-tabs-container').each(function () {
      var $container = $(this);
      var $firstButton = $container.find('.wiki-tab-button').first();
      var firstTabId = $firstButton.data('tab');
 
      $firstButton.addClass('active');
      $container.find('#' + firstTabId).addClass('active').show();
    });
 
    $('.wiki-tab-button').on('click', function () {
      var $button = $(this);
      var tabId = $button.data('tab');
      var $container = $button.closest('.wiki-tabs-container');
 
      $container.find('.wiki-tab-button').removeClass('active');
      $container.find('.wiki-tab-pane').removeClass('active').hide();
 
      $button.addClass('active');
      $container.find('#' + tabId).addClass('active').show();
    });
  });
 
  /* =========================================================
    Helpers: find/create containers in BOTH headers
    ========================================================= */
 
   function ensureDiscordLink(container, id) {
    if (!container) return null;
     if (document.getElementById(id)) return document.getElementById(id);


     const link = document.createElement('a');
     const link = document.createElement('a');
     link.id = 'custom-sticky-link';
     link.id = id;
     link.href = DISCORD_URL;
     link.href = DISCORD_URL;
     link.target = '_blank';
     link.target = '_blank';
     link.className =
     link.className =
       'cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet';
       'cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet sticky-discord-link';
     link.textContent = 'Join Us on Discord!';
     link.textContent = 'Join Us on Discord!';
    link.style.marginLeft = '10px';
     link.style.fontWeight = 'bold';
     link.style.fontWeight = 'bold';


     icons.prepend(link);
     container.prepend(link);
    return link;
   }
   }


$(document).ready(function () {
   // Sticky header: insert our widget bar inside sticky icons row
   $('.wiki-tabs-container').each(function () {
   function getStickyBar() {
    var $container = $(this);
    var $firstButton = $container.find('.wiki-tab-button').first();
    var firstTabId = $firstButton.data('tab');
 
    $firstButton.addClass('active');
    $container.find('#' + firstTabId).addClass('active').show();
  });
 
  $('.wiki-tab-button').on('click', function () {
    var $button = $(this);
    var tabId = $button.data('tab');
    var $container = $button.closest('.wiki-tabs-container');
 
    $container.find('.wiki-tab-button').removeClass('active');
    $container.find('.wiki-tab-pane').removeClass('active').hide();
 
    $button.addClass('active');
    $container.find('#' + tabId).addClass('active').show();
  });
});
 
 
/* =========================================================
  Sticky Header Widgets (Discord + Clock | Game Date | Countdown)
  CLEAN + FIXED:
  - No CSS inside JS (your old paste had CSS at the bottom of JS)
  - No   spacing hacks (spacing should be CSS)
  - One shared container for the 3 widgets
  ========================================================= */
 
  // Container that will hold ONLY the three widgets (not Discord, not default icons)
   function getStickyTimeContainer() {
     const icons = document.querySelector('.vector-sticky-header-icons');
     const icons = document.querySelector('.vector-sticky-header-icons');
     if (!icons) return null;
     if (!icons) return null;


     let mid = document.getElementById('sticky-time-widgets');
     let bar = document.getElementById('sticky-time-widgets');
     if (mid) return mid;
     if (bar) return bar;


     // Make sure Discord is created first so we can insert after it
     // Keep Discord where it was (far-left in sticky icons)
     ensureDiscordLink(icons);
     const discord = ensureDiscordLink(icons, 'custom-sticky-link');


     mid = document.createElement('span');
     bar = document.createElement('span');
     mid.id = 'sticky-time-widgets';
     bar.id = 'sticky-time-widgets';
     mid.className = 'sticky-time-widgets';
     bar.className = 'time-widgets';


     const discord = document.getElementById('custom-sticky-link');
     // Put widgets immediately AFTER Discord (so Discord stays at the left)
     if (discord && discord.parentNode === icons) {
     if (discord && discord.parentNode === icons) {
       discord.insertAdjacentElement('afterend', mid);
       discord.insertAdjacentElement('afterend', bar);
     } else {
     } else {
       icons.prepend(mid);
       icons.prepend(bar);
     }
     }


     return mid;
     return bar;
   }
   }


   function ensureClock(mid) {
  // Main header: put our widgets between search and username area
     if (document.getElementById('sticky-clock')) return;
   function getMainBar() {
     const header = document.querySelector('.vector-header');
    if (!header) return null;


     const wrap = document.createElement('span');
    // "End" is where user menu lives; we can safely insert at the start of it
     wrap.id = 'sticky-clock';
     const headerEnd = header.querySelector('.vector-header-end');
     wrap.className = 'sticky-clock header-widget';
     if (!headerEnd) return null;
 
     let bar = document.getElementById('main-time-widgets');
    if (bar) return bar;


     const label = document.createElement('span');
     bar = document.createElement('span');
     label.className = 'header-widget__label';
     bar.id = 'main-time-widgets';
     label.textContent = (CLOCK_TIMEZONE === 'Etc/UTC') ? 'UTC:' : 'Time:';
     bar.className = 'time-widgets';


     const value = document.createElement('span');
     // Add Discord + widgets inside the same main bar
     value.className = 'header-widget__value';
    headerEnd.prepend(bar);
    value.dataset.role = 'clock';
     ensureDiscordLink(bar, 'main-discord-link');


     wrap.appendChild(label);
     return bar;
    wrap.appendChild(value);
    mid.appendChild(wrap);
   }
   }


   function ensureGame(mid) {
  /* =========================================================
     if (document.getElementById('sticky-game-time')) return;
    Build widgets inside a bar (no duplicates)
    ========================================================= */
   function ensureWidget(bar, role, labelText) {
     if (!bar) return;


     const wrap = document.createElement('span');
     // If this bar already has this role, skip
     wrap.id = 'sticky-game-time';
     if (bar.querySelector(`[data-role="${role}"]`)) return;
    wrap.className = 'sticky-game-time header-widget';
 
    const value = document.createElement('span');
    value.className = 'header-widget__value';
    value.dataset.role = 'game';
 
    wrap.appendChild(value);
    mid.appendChild(wrap);
  }
 
  function ensureCountdown(mid) {
    if (document.getElementById('sticky-countdown')) return;


     const wrap = document.createElement('span');
     const wrap = document.createElement('span');
     wrap.id = 'sticky-countdown';
     wrap.className = 'header-widget';
     wrap.className = 'sticky-countdown header-widget';
     wrap.dataset.widget = role;


     const label = document.createElement('span');
     if (labelText) {
    label.className = 'header-widget__label';
      const label = document.createElement('span');
    label.textContent = 'Age ends in:';
      label.className = 'header-widget__label';
      label.textContent = labelText;
      wrap.appendChild(label);
    }


     const value = document.createElement('span');
     const value = document.createElement('span');
     value.className = 'header-widget__value';
     value.className = 'header-widget__value';
     value.dataset.role = 'countdown';
     value.dataset.role = role;


    wrap.appendChild(label);
     wrap.appendChild(value);
     wrap.appendChild(value);
     mid.appendChild(wrap);
     bar.appendChild(wrap);
   }
   }


  /* =========================================================
    Calculations / formatting
    ========================================================= */
   function formatRemaining(ms) {
   function formatRemaining(ms) {
     if (ms <= 0) return '00:00:00';
     if (ms <= 0) return '00:00:00';
Line 167: Line 165:
     const hoursPassed = Math.floor((nowMs - GAME_ANCHOR_REAL_UTC) / 3600000);
     const hoursPassed = Math.floor((nowMs - GAME_ANCHOR_REAL_UTC) / 3600000);


     // Day increments every hour
     // Day increments every hour (1..24)
     const dayIndex = (GAME_ANCHOR_DAY - 1) + hoursPassed;
     const dayIndex = (GAME_ANCHOR_DAY - 1) + hoursPassed;
     const dayNumber = ((dayIndex % 24) + 24) % 24 + 1;
     const dayNumber = ((dayIndex % 24) + 24) % 24 + 1;


     // Month increments every 24 hours
     // Month increments every 24 hours (Jan..Jul cycle)
     const daysPassed = Math.floor(dayIndex / 24);
     const daysPassed = Math.floor(dayIndex / 24);
    // Month cycles (Jan..Jul)
     const totalMonthIndex = GAME_ANCHOR_MONTH_INDEX + daysPassed;
     const totalMonthIndex = GAME_ANCHOR_MONTH_INDEX + daysPassed;
     const monthIndex = ((totalMonthIndex % 7) + 7) % 7;
     const monthIndex = ((totalMonthIndex % 7) + 7) % 7;
Line 196: Line 192:
     const now = Date.now();
     const now = Date.now();


     const clockVal = document.querySelector('#sticky-time-widgets [data-role="clock"]');
     // Update ALL widgets in BOTH headers by role
    if (clockVal) clockVal.textContent = clockFmt.format(new Date(now));
    document.querySelectorAll('[data-role="clock"]').forEach((el) => {
      el.textContent = clockFmt.format(new Date(now));
    });


     const gameVal = document.querySelector('#sticky-time-widgets [data-role="game"]');
     document.querySelectorAll('[data-role="game"]').forEach((el) => {
    if (gameVal) gameVal.textContent = computeGameString(now);
      el.textContent = computeGameString(now);
    });


     const countdownVal = document.querySelector('#sticky-time-widgets [data-role="countdown"]');
     document.querySelectorAll('[data-role="countdown"]').forEach((el) => {
    if (countdownVal) countdownVal.textContent = formatRemaining(COUNTDOWN_TARGET_UTC - now);
      el.textContent = formatRemaining(COUNTDOWN_TARGET_UTC - now);
    });
   }
   }


   mw.hook('wikipage.content').add(function () {
   /* =========================================================
     const icons = document.querySelector('.vector-sticky-header-icons');
    Boot: create + update widgets for BOTH headers
     if (!icons) return;
    ========================================================= */
 
  function buildBarsIfPossible() {
    const stickyBar = getStickyBar();
     const mainBar = getMainBar();
 
    // Sticky: no Discord inside bar (Discord is separate link to the left)
    if (stickyBar) {
      ensureWidget(stickyBar, 'clock', 'UTC:');
      ensureWidget(stickyBar, 'game', null);
      ensureWidget(stickyBar, 'countdown', 'Age ends in:');
    }
 
    // Main: Discord lives INSIDE bar (first), then widgets
     if (mainBar) {
      ensureWidget(mainBar, 'clock', 'UTC:');
      ensureWidget(mainBar, 'game', null);
      ensureWidget(mainBar, 'countdown', 'Age ends in:');
    }


     const mid = getStickyTimeContainer();
     updateAll();
    if (!mid) return;
  }


    ensureClock(mid);
  // Fires reliably for header DOM
    ensureGame(mid);
  mw.hook('skin.ready').add(function () {
     ensureCountdown(mid);
     buildBarsIfPossible();


    // Start update loop once
     if (!window.__timeWidgetsStarted) {
     if (!window.__stickyTimeWidgetsStarted) {
       window.__timeWidgetsStarted = true;
       window.__stickyTimeWidgetsStarted = true;
      updateAll();
       setInterval(updateAll, 1000);
       setInterval(updateAll, 1000);
    } else {
      // If navigating, ensure values refresh immediately
      updateAll();
     }
     }
  });
  // Fires on page navigation/content swaps
  mw.hook('wikipage.content').add(function () {
    buildBarsIfPossible();
   });
   });


})();
})();

Revision as of 11:55, 19 February 2026

/* Any JavaScript here will be loaded for all users on every page load. */

(function () {
  /* =========================================================
     CONFIG
     ========================================================= */
  const DISCORD_URL = 'https://discord.gg/t2Rp2dRvze';

  // Countdown target: Sat, 18 Apr 2026 00:00 UTC
  const COUNTDOWN_TARGET_UTC = Date.parse('2026-04-18T00:00:00Z');

  // Clock timezone (same for everyone)
  const CLOCK_TIMEZONE = 'Etc/UTC';

  // Game time anchor (real UTC -> game time)
  // At 2026-02-19 14:00:00 UTC, game time was Jan 21 YR2
  const GAME_ANCHOR_REAL_UTC = Date.parse('2026-02-19T14:00:00Z');
  const GAME_ANCHOR_MONTH_INDEX = 0; // Jan
  const GAME_ANCHOR_DAY = 21;        // 1..24
  const GAME_ANCHOR_YEAR = 2;        // YR2
  const GAME_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'];

  const pad2 = (n) => String(n).padStart(2, '0');

  /* =========================================================
     Tabs (your existing code)
     ========================================================= */
  $(document).ready(function () {
    $('.wiki-tabs-container').each(function () {
      var $container = $(this);
      var $firstButton = $container.find('.wiki-tab-button').first();
      var firstTabId = $firstButton.data('tab');

      $firstButton.addClass('active');
      $container.find('#' + firstTabId).addClass('active').show();
    });

    $('.wiki-tab-button').on('click', function () {
      var $button = $(this);
      var tabId = $button.data('tab');
      var $container = $button.closest('.wiki-tabs-container');

      $container.find('.wiki-tab-button').removeClass('active');
      $container.find('.wiki-tab-pane').removeClass('active').hide();

      $button.addClass('active');
      $container.find('#' + tabId).addClass('active').show();
    });
  });

  /* =========================================================
     Helpers: find/create containers in BOTH headers
     ========================================================= */

  function ensureDiscordLink(container, id) {
    if (!container) return null;
    if (document.getElementById(id)) return document.getElementById(id);

    const link = document.createElement('a');
    link.id = id;
    link.href = DISCORD_URL;
    link.target = '_blank';
    link.className =
      'cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet sticky-discord-link';
    link.textContent = 'Join Us on Discord!';
    link.style.fontWeight = 'bold';

    container.prepend(link);
    return link;
  }

  // Sticky header: insert our widget bar inside sticky icons row
  function getStickyBar() {
    const icons = document.querySelector('.vector-sticky-header-icons');
    if (!icons) return null;

    let bar = document.getElementById('sticky-time-widgets');
    if (bar) return bar;

    // Keep Discord where it was (far-left in sticky icons)
    const discord = ensureDiscordLink(icons, 'custom-sticky-link');

    bar = document.createElement('span');
    bar.id = 'sticky-time-widgets';
    bar.className = 'time-widgets';

    // Put widgets immediately AFTER Discord (so Discord stays at the left)
    if (discord && discord.parentNode === icons) {
      discord.insertAdjacentElement('afterend', bar);
    } else {
      icons.prepend(bar);
    }

    return bar;
  }

  // Main header: put our widgets between search and username area
  function getMainBar() {
    const header = document.querySelector('.vector-header');
    if (!header) return null;

    // "End" is where user menu lives; we can safely insert at the start of it
    const headerEnd = header.querySelector('.vector-header-end');
    if (!headerEnd) return null;

    let bar = document.getElementById('main-time-widgets');
    if (bar) return bar;

    bar = document.createElement('span');
    bar.id = 'main-time-widgets';
    bar.className = 'time-widgets';

    // Add Discord + widgets inside the same main bar
    headerEnd.prepend(bar);
    ensureDiscordLink(bar, 'main-discord-link');

    return bar;
  }

  /* =========================================================
     Build widgets inside a bar (no duplicates)
     ========================================================= */
  function ensureWidget(bar, role, labelText) {
    if (!bar) return;

    // If this bar already has this role, skip
    if (bar.querySelector(`[data-role="${role}"]`)) return;

    const wrap = document.createElement('span');
    wrap.className = 'header-widget';
    wrap.dataset.widget = role;

    if (labelText) {
      const label = document.createElement('span');
      label.className = 'header-widget__label';
      label.textContent = labelText;
      wrap.appendChild(label);
    }

    const value = document.createElement('span');
    value.className = 'header-widget__value';
    value.dataset.role = role;

    wrap.appendChild(value);
    bar.appendChild(wrap);
  }

  /* =========================================================
     Calculations / formatting
     ========================================================= */
  function formatRemaining(ms) {
    if (ms <= 0) return '00:00:00';

    const totalSeconds = Math.floor(ms / 1000);
    const days = Math.floor(totalSeconds / 86400);
    const hours = Math.floor((totalSeconds % 86400) / 3600);
    const minutes = Math.floor((totalSeconds % 3600) / 60);
    const seconds = totalSeconds % 60;

    if (days > 0) return `${days}d ${pad2(hours)}:${pad2(minutes)}:${pad2(seconds)}`;
    return `${pad2(hours)}:${pad2(minutes)}:${pad2(seconds)}`;
  }

  function computeGameString(nowMs) {
    const hoursPassed = Math.floor((nowMs - GAME_ANCHOR_REAL_UTC) / 3600000);

    // Day increments every hour (1..24)
    const dayIndex = (GAME_ANCHOR_DAY - 1) + hoursPassed;
    const dayNumber = ((dayIndex % 24) + 24) % 24 + 1;

    // Month increments every 24 hours (Jan..Jul cycle)
    const daysPassed = Math.floor(dayIndex / 24);
    const totalMonthIndex = GAME_ANCHOR_MONTH_INDEX + daysPassed;
    const monthIndex = ((totalMonthIndex % 7) + 7) % 7;

    // Year increments every 7 real days
    const yearsPassed = Math.floor(totalMonthIndex / 7);
    const year = GAME_ANCHOR_YEAR + yearsPassed;

    return `Current Game Date: ${GAME_MONTHS[monthIndex]} ${dayNumber} YR${year}`;
  }

  const clockFmt = new Intl.DateTimeFormat('en-GB', {
    timeZone: CLOCK_TIMEZONE,
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    hour12: false
  });

  function updateAll() {
    const now = Date.now();

    // Update ALL widgets in BOTH headers by role
    document.querySelectorAll('[data-role="clock"]').forEach((el) => {
      el.textContent = clockFmt.format(new Date(now));
    });

    document.querySelectorAll('[data-role="game"]').forEach((el) => {
      el.textContent = computeGameString(now);
    });

    document.querySelectorAll('[data-role="countdown"]').forEach((el) => {
      el.textContent = formatRemaining(COUNTDOWN_TARGET_UTC - now);
    });
  }

  /* =========================================================
     Boot: create + update widgets for BOTH headers
     ========================================================= */

  function buildBarsIfPossible() {
    const stickyBar = getStickyBar();
    const mainBar = getMainBar();

    // Sticky: no Discord inside bar (Discord is separate link to the left)
    if (stickyBar) {
      ensureWidget(stickyBar, 'clock', 'UTC:');
      ensureWidget(stickyBar, 'game', null);
      ensureWidget(stickyBar, 'countdown', 'Age ends in:');
    }

    // Main: Discord lives INSIDE bar (first), then widgets
    if (mainBar) {
      ensureWidget(mainBar, 'clock', 'UTC:');
      ensureWidget(mainBar, 'game', null);
      ensureWidget(mainBar, 'countdown', 'Age ends in:');
    }

    updateAll();
  }

  // Fires reliably for header DOM
  mw.hook('skin.ready').add(function () {
    buildBarsIfPossible();

    if (!window.__timeWidgetsStarted) {
      window.__timeWidgetsStarted = true;
      setInterval(updateAll, 1000);
    }
  });

  // Fires on page navigation/content swaps
  mw.hook('wikipage.content').add(function () {
    buildBarsIfPossible();
  });

})();