dreifachpunkt. / GeoGuessr League List Enhanced

// ==UserScript==
// @name GeoGuessr League List Enhanced
// @namespace    dreifachpunkt.
// @author dreifachpunkt.
// @description Display all your leagues in a table or calendar, ordered by the next leg's expiration date. Code Framework by Kommu.
// @version 0.1.10
// @include https://www.geoguessr.com/*
// @run-at document-start
// @license MIT
// @updateURL https://openuserjs.org/meta/dreifachpunkt./GeoGuessr_League_List_Enhanced.meta.js
// ==/UserScript==

/*jshint esversion: 6 */

// -------------------------------- SETTINGS ---------------------------------------------

// Hide the information on the top of the Pro Leagues page.
var hideInfo = false; //available: true, false

// This script features two different designs:
// (1) An OVERVIEW design in which each league only appears once and
// (2) a CALENDAR design in which every upcoming leg of each league is shown.
// You can set the design to be displayed first when you load the page.
var startDesign = "overview"; //available: overview, calendar

// You can select whether the "leg ends" column shows the expiration date, the remaining
// time or both (not for the compact calendar).
var legEndsFormat = "both"; //available: date, remaining, both

// Did you join a league but then decide that you do not want to play it? With this feature
// you can hide leagues. Click on the league and copy the last part of the URL into the
// array.
var hiddenLeagues = [];
//e.g. ["CPt0tzDt4fMUZcEL", "nUOfPCg358UZ0h5R"] for the Diverse World and NPMZ League 2022

// You can choose your preferred symbols or text for the different types of league statuses.
// E.g. use one of these symbols: ⚫⚪🔴🟠🟡🟢🔵🟣🟤⭕❌✅❎.
var symbolPlayed = "🟢";
var symbolNotPlayed = "🔴";
var symbolNotAvailable = "⚪";

// For those who have difficulties reading the new font, you can change it to Arial.
var arialFont = false; //available: true, false

// ------------------------- DESIGN-SPECIFIC SETTINGS ------------------------------------

// For the OVERVIEW, you can activate an advanced version to display additional
// information:
// (1) The time limit and game mode (moving, NM, NMPZ, etc.),
// (2) the duration of a leg and
// (3) the amount of players who already played the leg.
// The advanced overview takes a bit longer to load if you have many leagues.
// Also for smaller screen sizes (e.g. laptops) I do not recommend the advanced version.
var overviewAdvancedVersion = false; //available: true, false

// For the CALENDAR, you can choose between a compact and a list appearance.
// The compact calendar does not show leagues that are open for registration.
var calendarAppearance = "compact"; //available: compact, list

// For the COMPACT CALENDAR, you can display the current month or the upcoming two weeks.
var calendarCompactType = "weeks"; //available: weeks, month

// For the LIST CALENDAR, you can set the amount of entries to be shown in the table.
var calendarListHowManyCalEvents = 0; //available: 0 (= display all entries), 1, 2, ...

// ---------------------------------------------------------------------------------------
// If you encounter any bugs or errors, please contact me on discord: dreifachpunkt.#9954
// ---------------------------------------------------------------------------------------
// ---------------------- NO NEED TO EDIT BELOW THIS LINE --------------------------------
// ---------------------------------------------------------------------------------------

var version = "v0.1.10";
var accessibilityHighlightColor =
  'style="background-color: rgba(255, 255, 255, 0.1);"';
var buttonClicked = false;
var amountOfSoonExpiringLegsToBePlayed = 0;
var advancedVersion = overviewAdvancedVersion;
var calendarCompact = calendarAppearance === "compact" ? true : false;
var legEndsRemaining = legEndsFormat === "remaining" ? true : false;
var legEndsBoth = legEndsFormat === "both" ? true : false;
var listFormatCalendar = startDesign === "calendar" ? true : false;
var isCalWeek = calendarCompactType === "weeks" ? true : false;
const timeStrOptions = {
  hour: "numeric",
  minute: "numeric",
};
const dateStrOptionsCompactCal = {
  month: "numeric",
  day: "numeric",
};

var oldHref = document.location.href;
var tableRow = 0;

window.onload = (event) => {
  leaguesBehavior();

  var bodyList = document.querySelector("body"),
    observer = new MutationObserver(function (mutations) {
      mutations.forEach(function (mutation) {
        if (oldHref != document.location.href) {
          oldHref = document.location.href;
          leaguesBehavior();
        }
      });
    });

  var config = {
    childList: true,
    subtree: true,
  };

  observer.observe(bodyList, config);
};

function leaguesBehavior() {
  if (location.pathname !== "/leagues" || location.pathname.endsWith("#")) {
    return false;
  }

  // check if the account still has old design
  if (document.getElementsByClassName("title-logo").length === 0) {
    var textInfo = document.createElement("div");
    textInfo.setAttribute("id", "textInfo");
    textInfo.innerHTML =
      'Thank you for using the enhanced Pro Leagues script. <span style="color:red;"><b>Your account still has the old league page design, but the current version (' +
      version +
      ") of the script only supports the new dark mode design. Please use v0.1.8 until you have the new design. You can install the old version by copying the content of https://pastebin.com/cb06LGMZ into a new tampermonkey script.</b></span><br><br><hr><br><br>";
    document
      .getElementsByClassName("title--medium")[0]
      .parentElement.insertBefore(
        textInfo,
        document.getElementsByClassName("title--medium")[0]
      );
  }

  var style, table;

  if (!listFormatCalendar) {
    // table for overview mode
    style =
      "table.custom-table {\n" +
      "    border: 1px solid #e2e2e2;\n" +
      "    width: 100%;\n" +
      "    background-color: rgba(102, 0, 255, 0.1);" +
      "    border-collapse: collapse;\n" +
      "    margin-bottom: 40px;\n" +
      "        font-size: 12px;\n" +
      "}\n" +
      "\n" +
      "table.custom-table td, table.custom-table th {\n" +
      "    padding: 5px 10px;\n" +
      "    border: 1px solid #e2e2e2;\n" +
      "}\n" +
      "\n" +
      "table.custom-table th {\n" +
      "    text-transform: uppercase;\n" +
      "    font-size: 10px;\n" +
      "    color: #FCFAF9;\n" +
      "}\n" +
      "\n" +
      "table.custom-table td:nth-child(3) {\n" +
      "    max-width: 500px;\n" +
      "    word-break: break-word;\n" +
      "    text-align: center;\n" +
      "}\n" +
      "\n" +
      "table.custom-table td:nth-child(2) {\n" +
      "    text-align: center;\n" +
      "}\n" +
      "\n" +
      "table.custom-table td:nth-child(1),\n" +
      "table.custom-table td:nth-child(4),\n" +
      "table.custom-table td:nth-child(5),\n" +
      "table.custom-table td:nth-child(6) {\n" +
      "    text-align: center;\n" +
      "}\n" +
      (advancedVersion ?
        "table.custom-table td:nth-child(7),\n" +
        "table.custom-table td:nth-child(8) {\n" +
        "    text-align: center;\n" +
        "}\n" :
        "") +
      "\n";
    table =
      "<style>" +
      style +
      '</style><table class="custom-table" style="margin-bottom: 15px"><tr bgcolor="282828"><th>Creator</th><th>Name / Current map</th>' +
      (advancedVersion ? "<th>Mode</th><th>Leg duration</th>" : "") +
      "<th>Leg</th><th>Leg ends</th><th>played</th><th>Participants</th></tr>";
  }
  else {
    if (!calendarCompact) {
      style =
        "table.custom-table {\n" +
        "    border: 1px solid #e2e2e2;\n" +
        "    width: 100%;\n" +
        "    background-color: rgba(102, 0, 255, 0.1);" +
        "    border-collapse: collapse;\n" +
        "    margin-bottom: 40px;\n" +
        "        font-size: 12px;\n" +
        "}\n" +
        "\n" +
        "table.custom-table td, table.custom-table th {\n" +
        "    padding: 5px 10px;\n" +
        "    border: 1px solid #e2e2e2;\n" +
        "}\n" +
        "\n" +
        "table.custom-table th {\n" +
        "    text-transform: uppercase;\n" +
        "    font-size: 10px;\n" +
        "    color: #FCFAF9;\n" +
        "}\n" +
        "\n" +
        "table.custom-table td:nth-child(1),\n" +
        "table.custom-table td:nth-child(2),\n" +
        "table.custom-table td:nth-child(3),\n" +
        "table.custom-table td:nth-child(4),\n" +
        "table.custom-table td:nth-child(5) {\n" +
        "    text-align: center;\n" +
        "}\n" +
        "\n";
      table =
        "<style>" +
        style +
        '</style><table class="custom-table" style="margin-bottom: 15px"><tr bgcolor="282828"><th>Name</th><th>Map</th><th>Leg</th><th>Leg Ends</th><th>Played</th></tr>';
    }
    else {
      style =
        "table.custom-table {\n" +
        "    border: 1px solid #e2e2e2;\n" +
        "    width: 100%;\n" +
        "    background-color: rgba(102, 0, 255, 0.1);" +
        "    border-collapse: collapse;\n" +
        "    margin-bottom: 40px;\n" +
        "        font-size: 12px;\n" +
        "}\n" +
        "\n" +
        "table.custom-table td, table.custom-table th {\n" +
        "    padding: 5px 10px;\n" +
        "    border: 1px solid #e2e2e2;\n" +
        "width: 14%;\n" +
        "}\n" +
        "\n" +
        "table.custom-table th {\n" +
        "    text-transform: uppercase;\n" +
        "    font-size: 10px;\n" +
        "    color: #FCFAF9;\n" +
        "}\n" +
        "#theBestDiv {\n" +
        "    width: 90%;\n" +
        "    margin: auto;\n";
      "}\n" +
      "\n" +
      "table.custom-table td:nth-child(1),\n" +
      "table.custom-table td:nth-child(2),\n" +
      "table.custom-table td:nth-child(3),\n" +
      "table.custom-table td:nth-child(4),\n" +
      "table.custom-table td:nth-child(5),\n" +
      "table.custom-table td:nth-child(6),\n" +
      "table.custom-table td:nth-child(7) {\n" +
      "    text-align: center;\n" +
      "}\n" +
      "\n";
      table =
        "<style>" +
        style +
        '</style><table class="custom-table" style="margin-bottom: 15px"><tr bgcolor="282828"><th>Mon</th><th>Tue</th><th>Wen</th><th>Thu</th><th>Fri</th><th>Sat</th><th>Sun</th></tr>';
    }
  }

  var current = null;
  var page = 0;
  var dataBeforeSorting = [];
  var dataNotStarted = [];

  while (current === null || current % 10 === 0) {
    if (current === null) {
      current = 0;
    }
    var xmlhttp = new XMLHttpRequest();
    xmlhttp.open(
      "GET",
      "https://www.geoguessr.com/api/v3/leagues/started?page=" + page,
      false
    );
    xmlhttp.send();
    if (xmlhttp.status === 200) {
      var data = JSON.parse(xmlhttp.responseText);
      current = current + data.length;
      if (data.length === 0) {
        current++;
      }
      page++;
      dataBeforeSorting.push(data);
    }
    else {
      alert("An error occured, while getting maps.");
      return false;
    }
  }

  current = null;
  page = 0;

  while (current === null || current % 10 === 0) {
    if (current === null) {
      current = 0;
    }

    var xmlhttp2 = new XMLHttpRequest();
    xmlhttp2.open(
      "GET",
      "https://www.geoguessr.com/api/v3/leagues/notstarted?page=" + page,
      false
    );
    xmlhttp2.send();
    if (xmlhttp2.status === 200) {
      var data2 = JSON.parse(xmlhttp2.responseText);
      current = current + data2.length;
      if (data2.length === 0) {
        current++;
      }
      page++;
      dataNotStarted.push(data2);
    }
    else {
      alert("An error occured, while getting maps.");
      return false;
    }
  }

  //show finished leagues with the help of localStorage
  if (!buttonClicked) {
    let tokenListOfAllLeaguesInTheStorageToBeShown = [];
    let leaguesStorage = JSON.parse(localStorage.leaguesStorage || "{}");
    var showHorizontalLine = false;
    var leagueHasFinishedInfo = document.createElement("div");

    for (league in leaguesStorage) {
      if (leaguesStorage[league].expDate - Date.now() > 0) {
        //the league is in the localStorage, but not over yet
        continue;
      }
      showHorizontalLine = true;
      tokenListOfAllLeaguesInTheStorageToBeShown.push(
        leaguesStorage[league].token
      );
      var aFinishedLeague = document.createElement("span");
      aFinishedLeague.setAttribute("style", "color:red;");

      var linkSpan = document.createElement("span");
      linkSpan.setAttribute("id", leaguesStorage[league].token);
      linkSpan.innerHTML =
        '<b><a style="text-decoration: underline; color: red;" href="https://www.geoguessr.com/leagues/' +
        leaguesStorage[league].token +
        '">here</a></b>';

      aFinishedLeague.innerHTML =
        "<b>" +
        leaguesStorage[league].trueExpDate +
        " at " +
        leaguesStorage[league].trueExpTime +
        ': The league "' +
        leaguesStorage[league].name +
        '" finished after ' +
        leaguesStorage[league].totalLegs +
        (leaguesStorage[league].totalLegs === 1 ? " leg" : " legs") +
        ". Click <b>";

      aFinishedLeague.appendChild(linkSpan);
      aFinishedLeague.innerHTML += "<b> to see the results!</b><br><br>";

      leagueHasFinishedInfo.appendChild(aFinishedLeague);
      console.log(leagueHasFinishedInfo);
    }
    if (showHorizontalLine) {
      leagueHasFinishedInfo.innerHTML += "<hr><br><br>";
    }

    document
      .getElementsByClassName("title-logo")[0]
      .parentElement.insertBefore(
        leagueHasFinishedInfo,
        document.getElementsByClassName("title-logo")[0]
      );

    tokenListOfAllLeaguesInTheStorageToBeShown.forEach((el) => {
      document.getElementById(el).addEventListener(
        "click",
        () => {
          deleteStorageEntry(el);
        },
        false
      );
    });
  }

  //show script info
  if (!hideInfo && !buttonClicked) {
    var textInfo = document.createElement("div");
    textInfo.setAttribute("id", "textInfo");
    textInfo.innerHTML =
      'Thank you for using the enhanced Pro Leagues script. <span style="color:red;"><b>To hide this info text, visit the settings at the top of the script and set the variable "hideInfo" to true.</b></span> There you will also find all the customization options.<br><br>New in ' +
      version +
      ":<br>- Adapts to changed API results of finished current legs.<br><br><hr><br><br>";
    document
      .getElementsByClassName("title-logo")[0]
      .parentElement.insertBefore(
        textInfo,
        document.getElementsByClassName("title-logo")[0]
      );
  }

  if (!listFormatCalendar) {
    document.getElementsByClassName("title-logo")[0].innerText =
      "Pro Leagues Enhanced";
    table += getHtmlOverview(dataBeforeSorting);
    table += getHtmlNotStarted(dataNotStarted);
    document.title =
      "Pro Leagues Enhanced" +
      (amountOfSoonExpiringLegsToBePlayed > 0 ?
        " (" + amountOfSoonExpiringLegsToBePlayed + ")" :
        "");
  }
  else {
    document.getElementsByClassName("title-logo")[0].innerText =
      "Pro Leagues Calendar";
    table += getHtmlCalendar(dataBeforeSorting);
    if (!calendarCompact) {
      table += getHtmlNotStarted(dataNotStarted);
    }
    document.title =
      "Pro Leagues Calendar" +
      (amountOfSoonExpiringLegsToBePlayed > 0 ?
        " (" + amountOfSoonExpiringLegsToBePlayed + ")" :
        "");
  }

  table +=
    '</table><div style="margin-bottom: 15px; color:#ACAAA9; font-size: 12px; float: right">by dreifachpunkt. | ' +
    version +
    "</div>";
  table +=
    '<input type="button" id="btnChangeDesign" value="' +
    (listFormatCalendar ? "Go to Overview" : "Go to Calendar") +
    '" class="button button--small button--primary" style="margin-bottom: 15px; float: left"/>';
  table +=
    listFormatCalendar && calendarCompact ?
    ' <input type="button" id="btnChangeCalendar" value="' +
    (isCalWeek ? "Show Month" : "Show 2 Weeks") +
    '" class="button button--small button--secondary" style="margin-bottom: 15px; float: left; margin-left: 10px"/>' :
    "";
  var div = document.createElement("div");
  div.setAttribute("id", "theBestDiv");
  div.innerHTML = table;
  //div.setAttribute("style", "width=90%;");
  if (arialFont) {
    div.style.fontFamily = "Arial, sans-serif";
  }

  var elements = document.getElementsByClassName("container__content");

  var oldDiv = document.getElementById("theBestDiv");
  if (!buttonClicked) {
    elements[0].parentNode.insertBefore(div, elements[0]);
    elements[0].parentNode.removeChild(elements[0]);
    // elements[0].innerHTML = "";
  }
  else {
    if (oldDiv !== null) {
      oldDiv.parentNode.replaceChild(div, oldDiv);
    }
    else {
      elements[0].parentNode.insertBefore(div, elements[0]);
      elements[0].parentNode.removeChild(elements[0]);
      //elements[0].innerHTML = "";
    }
  }

  document
    .getElementById("btnChangeDesign")
    .addEventListener("click", changeDesign, false);
  if (document.getElementById("btnChangeCalendar") != null) {
    document
      .getElementById("btnChangeCalendar")
      .addEventListener("click", changeCalendar, false);
  }
}

function changeDesign() {
  listFormatCalendar = listFormatCalendar ? false : true;
  tableRow = 0;
  buttonClicked = true;
  amountOfSoonExpiringLegsToBePlayed = 0;
  leaguesBehavior();
}

function changeCalendar() {
  isCalWeek = isCalWeek ? false : true;
  tableRow = 0;
  buttonClicked = true;
  amountOfSoonExpiringLegsToBePlayed = 0;
  leaguesBehavior();
}

function addStorageEntry(
  token,
  name,
  totalLegs,
  expDate,
  trueExpDate,
  trueExpTime
) {
  let leaguesStorage = JSON.parse(localStorage.leaguesStorage || "{}");
  leaguesStorage[token] = {
    name,
    totalLegs,
    expDate,
    trueExpDate,
    trueExpTime,
    token,
  };
  localStorage.leaguesStorage = JSON.stringify(leaguesStorage);
}

function deleteStorageEntry(token) {
  console.log("delete entry");
  let leaguesStorage = JSON.parse(localStorage.leaguesStorage || "{}");
  delete leaguesStorage[token];
  localStorage.leaguesStorage = JSON.stringify(leaguesStorage);
}

function getHtmlOverview(data) {
  var output = "";
  var dataSorted = [];

  // combine multiple arrays
  for (let arr in data) {
    for (let objInner in data[arr]) {
      if (data[arr][objInner].league.currentLeg != null) {
        //if the league is ending and the last scoring is awaited, the status is still "started" but the currentLeg is null
        dataSorted.push(data[arr][objInner]);
      }
    }
  }

  // sort by current leg ending
  for (var i = 0; i < dataSorted.length; i++) {
    for (var j = 0; j < dataSorted.length - 1; j++) {
      if (
        dataSorted[j].league.currentLeg.endsAt >
        dataSorted[j + 1].league.currentLeg.endsAt
      ) {
        var temp = dataSorted[j];
        dataSorted[j] = dataSorted[j + 1];
        dataSorted[j + 1] = temp;
      }
    }
  }

  var now = Date.now(); // returns millis

  for (let obj in dataSorted) {
    var league = dataSorted[obj];

    // skip hidden leagues
    var isHidden = false;
    for (i = 0; i < hiddenLeagues.length; i++) {
      if (league.league.token === hiddenLeagues[i]) {
        isHidden = true;
      }
    }
    if (isHidden) {
      continue;
    }

    // modify league object to have necessary information
    var startDate = Date.parse(league.league.currentLeg.startsAt);
    var expDate = Date.parse(league.league.currentLeg.endsAt);

    league.league.currentLeg.duration =
      getDuration(startDate, expDate)[0] +
      " " +
      getDuration(startDate, expDate)[1];

    league.league.currentLeg.remainingTime =
      getDuration(now, expDate)[0] + " " + getDuration(now, expDate)[1];
    var trueExpDate = new Date(expDate);
    league.league.currentLeg.trueExpDate = trueExpDate.toLocaleDateString();
    league.league.currentLeg.trueExpTime = trueExpDate.toLocaleTimeString(
      undefined,
      timeStrOptions
    );
    league.league.currentLeg.nextLegSoon =
      (expDate - now) / 1000 / 60 / 60 < 24;
    if (
      league.league.currentLeg.nextLegSoon &&
      !league.hasUserFinishedCurrentLeg.result
    ) {
      amountOfSoonExpiringLegsToBePlayed++;
    }
    tableRow++;
    var currentChallenge = {};

    if (advancedVersion) {
      var xmlhttpLeagueSpecific = new XMLHttpRequest();
      xmlhttpLeagueSpecific.open(
        "GET",
        "https://www.geoguessr.com/api/v3/challenges/" +
        league.league.currentLeg.challengeId,
        false
      );
      xmlhttpLeagueSpecific.send();
      if (xmlhttpLeagueSpecific.status === 200) {
        var dataLeagueSpecific = JSON.parse(xmlhttpLeagueSpecific.responseText);
        currentChallenge = dataLeagueSpecific;
      }
      else {
        alert("An error occured, while getting a specific challenge.");
        return false;
      }
    }

    // wenn letztes Leg: erstelle Entry in localStorage, damit League angezeigt wird wenn sie endet
    if (league.league.totalLegs === league.league.currentLeg.legNumber) {
      addStorageEntry(
        league.league.token,
        league.league.name,
        league.league.totalLegs,
        expDate,
        league.league.currentLeg.trueExpDate,
        league.league.currentLeg.trueExpTime
      );
    }

    output += getOutputStrOverview(league, currentChallenge);
  }

  return output;
}

function getHtmlNotStarted(data) {
  var output = "";

  var dataSorted = [];

  for (let arr in data) {
    for (let objInner in data[arr]) {
      dataSorted.push(data[arr][objInner]);
    }
  }

  // check whether there exists a league that is not hidden
  var existsLeagueThatIsNotHidden = false;
  for (let obj in dataSorted) {
    var league = dataSorted[obj];
    if (!hiddenLeagues.includes(league.league.token)) {
      existsLeagueThatIsNotHidden = true;
    }
  }

  if (dataSorted.length > 0 && existsLeagueThatIsNotHidden) {
    tableRow++;

    if (!listFormatCalendar) {
      output +=
        "<tr " +
        (tableRow % 2 === 0 ?
          'style="background-color: rgba(255, 255, 255, 0.1);"' :
          "") +
        "><td></td><td></td><td></td><td></td>" +
        (advancedVersion ? "<td></td><td></td>" : "") +
        "<td></td><td></td></tr>";
    }
    else {
      output +=
        "<tr " +
        (tableRow % 2 === 0 ?
          'style="background-color: rgba(255, 255, 255, 0.1);"' :
          "") +
        "><td></td><td></td><td></td><td></td><td></td></tr>";
    }
  }

  for (var i = 0; i < dataSorted.length; i++) {
    for (var j = 0; j < dataSorted.length - 1; j++) {
      if (
        dataSorted[j].league.numberOfParticipants <
        dataSorted[j + 1].league.numberOfParticipants
      ) {
        var temp = dataSorted[j];
        dataSorted[j] = dataSorted[j + 1];
        dataSorted[j + 1] = temp;
      }
    }
  }

  for (let obj in dataSorted) {
    var league = dataSorted[obj];

    var isHidden = false;
    for (i = 0; i < hiddenLeagues.length; i++) {
      if (league.league.token === hiddenLeagues[i]) {
        isHidden = true;
      }
    }
    if (isHidden) {
      continue;
    }

    tableRow++;

    output += getOutputStrNotStarted(league);
  }
  return output;
}

//this function also returns the output string of the compact calendar
function getHtmlCalendar(data) {
  var output = "";
  var dataSorted = [];

  for (let arr in data) {
    for (let objInner in data[arr]) {
      if (data[arr][objInner].league.currentLeg != null) {
        // league is ending
        dataSorted.push(data[arr][objInner]);
      }
    }
  }

  var now = Date.now(); // returns millis
  var additionalArtificialLeagues = [];
  var additionalArtificialPreviousLeagues = [];

  for (var it = 0; it < dataSorted.length; it++) {
    var league = dataSorted[it];
    league.league.currentLeg.endsAt = Date.parse(
      league.league.currentLeg.endsAt
    );
    league.league.currentLeg.startsAt = Date.parse(
      league.league.currentLeg.startsAt
    );
    var durationMillis =
      league.league.currentLeg.endsAt - league.league.currentLeg.startsAt;

    league.league.currentLeg.remainingTime =
      getDuration(now, league.league.currentLeg.endsAt)[0] +
      " " +
      getDuration(now, league.league.currentLeg.endsAt)[1];
    var trueExpDateOriginal = new Date(league.league.currentLeg.endsAt);
    league.league.currentLeg.trueExpDate =
      trueExpDateOriginal.toLocaleDateString();
    league.league.currentLeg.trueExpTime =
      trueExpDateOriginal.toLocaleTimeString(undefined, timeStrOptions);

    league.league.currentLeg.hasStarted = true;
    league.league.currentLeg.nextLegSoon =
      (league.league.currentLeg.endsAt - now) / 1000 / 60 / 60 < 24;
    if (
      league.league.currentLeg.nextLegSoon &&
      !league.hasUserFinishedCurrentLeg.result
    ) {
      amountOfSoonExpiringLegsToBePlayed++;
    }

    if (league.league.totalLegs === league.league.currentLeg.legNumber) {
      addStorageEntry(
        league.league.token,
        league.league.name,
        league.league.totalLegs,
        league.league.currentLeg.endsAt,
        league.league.currentLeg.trueExpDate,
        league.league.currentLeg.trueExpTime
      );
    }

    //calculate future legs of a league by creating additional leagues with the
    //current leg ending in the future
    for (
      var legNr = league.league.currentLeg.legNumber; legNr < league.league.totalLegs; legNr++
    ) {
      var artificialLeague = {};
      artificialLeague.hasUserFinishedCurrentLeg = {};
      artificialLeague.hasUserFinishedCurrentLeg.result = false;
      artificialLeague.league = {};
      artificialLeague.league.name = league.league.name;
      artificialLeague.league.token = league.league.token;
      artificialLeague.league.currentLeg = {};
      artificialLeague.league.currentLeg.mapSlug = "";
      artificialLeague.league.currentLeg.hasStarted = false;
      artificialLeague.league.currentLeg.mapName = "t.b.a.";
      artificialLeague.league.currentLeg.legNumber = legNr + 1;
      artificialLeague.league.totalLegs = league.league.totalLegs;
      artificialLeague.league.currentLeg.endsAt =
        league.league.currentLeg.endsAt +
        (legNr - league.league.currentLeg.legNumber + 1) * durationMillis;

      artificialLeague.league.currentLeg.nextLegSoon =
        (artificialLeague.league.currentLeg.endsAt - now) / 1000 / 60 / 60 < 24;
      artificialLeague.league.currentLeg.remainingTime =
        getDuration(now, artificialLeague.league.currentLeg.endsAt)[0] +
        " " +
        getDuration(now, artificialLeague.league.currentLeg.endsAt)[1];
      var trueExpDate = new Date(artificialLeague.league.currentLeg.endsAt);
      artificialLeague.league.currentLeg.trueExpDate =
        trueExpDate.toLocaleDateString();
      artificialLeague.league.currentLeg.trueExpTime =
        trueExpDate.toLocaleTimeString(undefined, timeStrOptions);

      additionalArtificialLeagues.push(artificialLeague);
    }

    //calculate past legs of a league by creating additional leagues with the
    //current leg ending in the past
    if (calendarCompact) {
      for (legNr = league.league.currentLeg.legNumber; legNr > 1; legNr--) {
        artificialLeague = {};
        artificialLeague.hasUserFinishedCurrentLeg = {};
        artificialLeague.hasUserFinishedCurrentLeg.result = true;
        artificialLeague.league = {};
        artificialLeague.league.name = league.league.name;
        artificialLeague.league.token = league.league.token;
        artificialLeague.league.currentLeg = {};
        artificialLeague.league.currentLeg.mapSlug = "";
        artificialLeague.league.currentLeg.hasStarted = false;
        artificialLeague.league.currentLeg.mapName = "Leg has already finished";
        artificialLeague.league.currentLeg.legNumber = legNr - 1;
        artificialLeague.league.totalLegs = league.league.totalLegs;
        artificialLeague.league.currentLeg.endsAt =
          league.league.currentLeg.endsAt +
          (legNr - league.league.currentLeg.legNumber - 1) * durationMillis;

        artificialLeague.league.currentLeg.nextLegSoon =
          (artificialLeague.league.currentLeg.endsAt - now) / 1000 / 60 / 60 <
          24;
        artificialLeague.league.currentLeg.remainingTime =
          "Leg has already finished";
        trueExpDate = new Date(artificialLeague.league.currentLeg.endsAt);
        artificialLeague.league.currentLeg.trueExpDate =
          trueExpDate.toLocaleDateString(undefined, dateStrOptionsCompactCal);
        artificialLeague.league.currentLeg.trueExpTime =
          trueExpDate.toLocaleTimeString(undefined, timeStrOptions);

        additionalArtificialLeagues.push(artificialLeague);
      }
    }
  }

  for (var ite = 0; ite < additionalArtificialLeagues.length; ite++) {
    dataSorted.push(additionalArtificialLeagues[ite]);
  }

  for (var i = 0; i < dataSorted.length; i++) {
    for (var j = 0; j < dataSorted.length - 1; j++) {
      if (
        dataSorted[j].league.currentLeg.endsAt >
        dataSorted[j + 1].league.currentLeg.endsAt
      ) {
        var temp = dataSorted[j];
        dataSorted[j] = dataSorted[j + 1];
        dataSorted[j + 1] = temp;
      }
    }
  }

  if (!calendarCompact) {
    for (let obj in dataSorted) {
      var leag = dataSorted[obj];
      if (
        tableRow >= calendarListHowManyCalEvents &&
        calendarListHowManyCalEvents !== 0
      ) {
        break;
      }
      var isHidden = false;
      for (i = 0; i < hiddenLeagues.length; i++) {
        if (leag.league.token === hiddenLeagues[i]) {
          isHidden = true;
        }
      }
      if (isHidden) {
        continue;
      }

      output += getOutputStrCalendarList(leag);
    }
  }
  else {
    //here comes the big mess :D
    if (dataSorted.length === 0) {
      return "";
    }

    var tempOut = "";
    var arrOfArrs = [];
    var tempArr = [];

    var dayMillisIter = getMillisOfDateStart(
      dataSorted[0].league.currentLeg.endsAt
    );

    i = 0;
    while (i <= dataSorted.length) {
      if (i >= dataSorted.length) {
        arrOfArrs.push(tempArr);
        tempArr = [];
        break;
      }

      isHidden = false;
      for (j = 0; j < hiddenLeagues.length && i < dataSorted.length; j++) {
        if (dataSorted[i].league.token === hiddenLeagues[j]) {
          isHidden = true;
        }
      }
      if (isHidden) {
        i++;
        continue;
      }

      if (tempArr.length === 0) {
        tempArr.push(dayMillisIter);
        dayMillisIter = getNextDayStartFromCurrentDateStart(dayMillisIter);
      }
      if (
        tempArr[0] ===
        getMillisOfDateStart(dataSorted[i].league.currentLeg.endsAt)
      ) {
        tempArr.push(dataSorted[i]);
        i++;
      }
      else {
        arrOfArrs.push(tempArr);
        tempArr = [];
      }
    }

    if (arrOfArrs.length > 1) {
      var firstArrSpot = arrOfArrs[0];
      var tempTime = firstArrSpot[0];

      for (i = 0; i < 42; i++) {
        tempTime = getPreviousDayStartFromCurrentDateStart(tempTime);
        arrOfArrs.unshift([tempTime]);
      }
    }

    var firstField = !isCalWeek ?
      new Date(getFirstDayOfWeek(getFirstDayOfMonth(Date.now()))) :
      new Date(getFirstDayOfWeek(Date.now()));
    var current = 0;

    while (arrOfArrs[current][0] < firstField) {
      current++;
      if (current === arrOfArrs.length) {
        return "";
      }
    }

    var dayOfWeek = 0;

    var amountOfDays = isCalWeek ? 14 : 42;

    for (i = current; i < current + amountOfDays; i++) {
      if (dayOfWeek === 7) {
        dayOfWeek = 0;
      }
      if (dayOfWeek === 0) {
        output += '<tr style="background-color: rgba(255, 255, 255, 0.1);">';
        tempOut += "<tr>";
      }

      if (arrOfArrs[i] == undefined || arrOfArrs[i].length == 0) {
        output += "<td></td>";
        tempOut += "<td>";
      }
      else {
        output +=
          "<td " +
          (getMillisOfDateStart(Date.now()) === arrOfArrs[i][0] ?
            'style="background-color: rgba(255, 255, 255, 0.3);"' :
            " ") +
          ">" +
          new Date(arrOfArrs[i][0]).toLocaleDateString() +
          "</td>";
        tempOut +=
          '<td style="text-align: left; vertical-align: top;' +
          (getMillisOfDateStart(Date.now()) === arrOfArrs[i][0] ?
            "background-color: rgba(255, 255, 255, 0.3);" :
            !isCalWeek ?
            new Date(arrOfArrs[i][0]).getMonth() !==
            new Date(Date.now()).getMonth() ?
            "background-color: rgba(255, 255, 255, 0.1);" :
            "" :
            "") +
          ' ">';

        for (j = 1; j < arrOfArrs[i].length; j++) {
          tempOut +=
            '<b style="display: inline-flex;">' +
            arrOfArrs[i][j].league.currentLeg.trueExpTime +
            "</b> " +
            (arrOfArrs[i][j].league.currentLeg.legNumber ===
              arrOfArrs[i][j].league.totalLegs ?
              '<span style="color:DeepSkyBlue;"><b><i>LAST LEG</i></b> </span>' :
              "") +
            (isCalWeek ? "<br>" : "") +
            '<a style="font-size: 10px;display: inline-flex; color: ' +
            (arrOfArrs[i][j].hasUserFinishedCurrentLeg.result ?
              "white" :
              "red") +
            ";" +
            (arrOfArrs[i][j].league.currentLeg.hasStarted ?
              "text-decoration: underline" :
              "") +
            ';" href="https://www.geoguessr.com/leagues/' +
            arrOfArrs[i][j].league.token +
            '">' +
            arrOfArrs[i][j].league.name +
            "</a><br>";
        }
      }

      tempOut += "</td>";

      if (dayOfWeek === 6) {
        output += "</tr>";
        tempOut += "</tr>";
        output += tempOut;
        tempOut = "";
      }

      dayOfWeek++;
    }
  }

  return output;
}

function getOutputStrOverview(league, currentChallenge) {
  // if normal overview, currentChallenge is {}
  var out =
    '<div class="tablerowdiv" ><tr ' +
    (tableRow % 2 === 0 ?
      'style="background-color: rgba(255, 255, 255, 0.1);"' :
      "") +
    "><td>" +
    getTableLeagueCreator(league) +
    "</td><td>" +
    getTableLeagueName(league) +
    "<br>" +
    getTableLeagueCurrMap(league) +
    "</td>" +
    (advancedVersion ?
      "<td>" +
      getTableLeagueMode(currentChallenge) +
      "</td><td>" +
      getTableLeagueDuration(league) +
      "</td>" :
      "") +
    "<td>" +
    getTableLeagueLeg(league) +
    "</td>" +
    "<td " +
    (league.league.currentLeg.nextLegSoon &&
      !league.hasUserFinishedCurrentLeg.result ?
      'style="color:red;">' :
      ">") +
    getTableLeagueLegEnds(league) +
    "</td>" +
    "<td>" +
    getTableLeaguePlayed(league) +
    "</td>" +
    "<td>" +
    getTableLeagueParticipants(league, currentChallenge) +
    "</td>" +
    "</tr></div>";

  return out;
}

function getOutputStrCalendarList(league) {
  var output = "";
  tableRow++;

  output +=
    "<tr " +
    (tableRow % 2 === 0 ?
      'style="background-color: rgba(255, 255, 255, 0.1);"' :
      "") +
    ">" +
    "<td>" +
    getTableLeagueName(league) +
    "</td>" +
    "<td>" +
    (league.league.currentLeg.hasStarted ?
      '<a style="color:white;" href="https://www.geoguessr.com/maps/' +
      league.league.currentLeg.mapSlug +
      '">' :
      "") +
    league.league.currentLeg.mapName +
    (league.league.currentLeg.hasStarted ? "</a>" : "") +
    "</td>" +
    "<td>" +
    getTableLeagueLeg(league) +
    "</td>" +
    "<td " +
    (league.league.currentLeg.nextLegSoon &&
      !league.hasUserFinishedCurrentLeg.result ?
      'style="color:red;">' :
      ">") +
    getTableLeagueLegEnds(league) +
    "</td>" +
    "<td>" +
    getTableLeaguePlayed(league) +
    "</td>" +
    "</tr>";

  return output;
}

function getOutputStrNotStarted(league) {
  var out = "";

  if (!listFormatCalendar) {
    out +=
      "<tr " +
      (tableRow % 2 === 0 ?
        'style="background-color: rgba(255, 255, 255, 0.1);"' :
        "") +
      ">" +
      "<td>" +
      getTableLeagueCreator(league) +
      "</td>" +
      "<td>" +
      getTableLeagueName(league) +
      "</td>" +
      (advancedVersion ? "<td></td><td></td>" : "") +
      "<td>0 of " +
      league.league.totalLegs +
      "</td>" +
      "<td>League has not started yet</td>" +
      "<td>" +
      symbolNotAvailable +
      "</td>" +
      "<td>" +
      league.league.numberOfParticipants +
      "</td>" +
      "</tr>";
  }
  else {
    out +=
      "<tr " +
      (tableRow % 2 === 0 ?
        'style="background-color: rgba(255, 255, 255, 0.1);"' :
        "") +
      ">" +
      "<td>" +
      getTableLeagueName(league) +
      "</td>" +
      "<td></td>" +
      "<td>0 of " +
      league.league.totalLegs +
      "</td>" +
      "<td>League has not started yet</td>" +
      "<td>" +
      symbolNotAvailable +
      "</td>" +
      "</tr>";
  }

  return out;
}

function getTableLeagueCreator(league) {
  return (
    '<a href="https://www.geoguessr.com' +
    league.league.creator.url +
    '" style="color: white;">' +
    league.league.creator.nick +
    "</a>"
  );
}

function getTableLeagueName(league) {
  return (
    '<b><a href="https://www.geoguessr.com/leagues/' +
    league.league.token +
    '" style="text-decoration: underline;color: white;">' +
    league.league.name +
    "</a></b>"
  );
}

function getTableLeagueCurrMap(league) {
  return (
    '<a href="https://www.geoguessr.com/maps/' +
    league.league.currentLeg.mapSlug +
    '" style="color: white;">' +
    league.league.currentLeg.mapName +
    "</a>"
  );
}

function getTableLeagueMode(currentChallenge) {
  return (
    (currentChallenge.challenge.timeLimit > 60 ?
      +(
        Math.round(currentChallenge.challenge.timeLimit / 60 + "e+2") + "e-2"
      ) + " mins per round" :
      currentChallenge.challenge.timeLimit === 0 ?
      "no time limit" :
      currentChallenge.challenge.timeLimit + " secs per round") +
    (currentChallenge.challenge.forbidMoving ||
      currentChallenge.challenge.forbidZooming ||
      currentChallenge.challenge.forbidRotating ?
      " (N" :
      "") +
    (currentChallenge.challenge.forbidMoving ? "M" : "") +
    (currentChallenge.challenge.forbidRotating ? "P" : "") +
    (currentChallenge.challenge.forbidZooming ? "Z" : "") +
    (currentChallenge.challenge.forbidMoving ||
      currentChallenge.challenge.forbidZooming ||
      currentChallenge.challenge.forbidRotating ?
      ")" :
      "")
  );
}

function getTableLeagueDuration(league) {
  return league.league.currentLeg.duration;
}

function getTableLeagueLeg(league) {
  return (
    league.league.currentLeg.legNumber +
    " of " +
    league.league.totalLegs +
    (league.league.currentLeg.legNumber === league.league.totalLegs ?
      '<br><span style="color:DeepSkyBlue;"><b><i>LAST LEG</i></b></span>' :
      "")
  );
}

function getTableLeagueLegEnds(league) {
  return (
    (!legEndsRemaining || legEndsBoth ?
      league.league.currentLeg.trueExpDate +
      " at " +
      league.league.currentLeg.trueExpTime :
      "") +
    (legEndsBoth ? "<br>" : "") +
    (legEndsRemaining || legEndsBoth ?
      league.league.currentLeg.remainingTime :
      "")
  );
}

function getTableLeaguePlayed(league) {
  var startLinkString = '<a href="https://www.geoguessr.com/challenge/';

  return !listFormatCalendar ?
    league.hasUserFinishedCurrentLeg.result ?
    startLinkString +
    league.league.currentLeg.challengeId +
    '">' +
    symbolPlayed +
    "</a>" :
    startLinkString +
    league.league.currentLeg.challengeId +
    '">' +
    symbolNotPlayed +
    "</a>" :
    league.league.currentLeg.hasStarted ?
    league.hasUserFinishedCurrentLeg.result ?
    startLinkString +
    league.league.currentLeg.challengeId +
    '">' +
    symbolPlayed +
    "</a>" :
    startLinkString +
    league.league.currentLeg.challengeId +
    '">' +
    symbolNotPlayed +
    "</a>" :
    symbolNotAvailable;
}

function getTableLeagueParticipants(league, currentChallenge) {
  return advancedVersion ?
    currentChallenge.challenge.numberOfParticipants +
    (league.hasUserFinishedCurrentLeg.result ? true : false) +
    " of " +
    league.league.numberOfParticipants +
    " played" :
    league.league.numberOfParticipants;
}

function getFirstDayOfWeek(dateMillis) {
  var dayOfWeek =
    new Date(dateMillis).getDay() === 0 ? 7 : new Date(dateMillis).getDay();
  var retVal = getMillisOfDateStart(dateMillis);
  while (dayOfWeek > 1) {
    retVal = getPreviousDayStartFromCurrentDateStart(retVal);
    dayOfWeek--;
  }
  return retVal;
}

function getFirstDayOfMonth(dateMillis) {
  var dayOfMonth = new Date(dateMillis).getDate();
  var retVal = getMillisOfDateStart(dateMillis);
  while (dayOfMonth > 1) {
    retVal = getPreviousDayStartFromCurrentDateStart(retVal);
    dayOfMonth--;
  }
  return retVal;
}

function getDayMillis() {
  return 1 * 1000 * 60 * 60 * 24;
}

function getPreviousDayStartFromCurrentDateStart(dateMillis) {
  return getMillisOfDateStart(dateMillis - 1 * 1000 * 60 * 60 * 22);
}

function getNextDayStartFromCurrentDateStart(dateMillis) {
  return getMillisOfDateStart(dateMillis + 1 * 1000 * 60 * 60 * 26);
}

function getMillisOfDateStart(dateMillis) {
  var date = new Date(dateMillis);
  var dateStart = new Date(
    date.getFullYear(),
    date.getMonth(),
    date.getDate(),
    0,
    0,
    0,
    0
  );
  return Date.parse(dateStart);
}

function getDuration(startDate, endDate) {
  //both in millis
  var durationHours = (endDate - startDate) / 1000 / 60 / 60;

  var retArray = [0, ""];

  switch (true) {
    case durationHours < 1:
      retArray[0] = Math.floor(durationHours * 60);
      retArray[1] = "minutes";
      break;
    case durationHours === 1:
      retArray[0] = durationHours;
      retArray[1] = "hour";
      break;
    case durationHours < 24:
      retArray[0] = Math.floor(durationHours);
      retArray[1] = Math.floor(durationHours) === 1 ? "hour" : "hours";
      break;
    case durationHours >= 24 && durationHours < 48:
      retArray[0] = Math.floor(durationHours / 24);
      retArray[1] = "day";
      break;
    case durationHours > 24 && durationHours < 24 * 7:
    default:
      retArray[0] = Math.floor(durationHours / 24);
      retArray[1] = "days";
      break;
    case durationHours >= 24 * 7 && durationHours < 24 * 7 * 2:
      retArray[0] = Math.floor(durationHours / 24 / 7);
      retArray[1] = "week";
      break;
    case durationHours > 24 * 7:
      retArray[0] = Math.floor(durationHours / 24 / 7);
      retArray[1] = "weeks";
      break;
  }
  return retArray;
}