Leetcode contest table

Get a better understanding of how you have performed across different contests, by getting a tabular view

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         Leetcode contest table
// @namespace    http://tampermonkey.net/
// @version      0.0.3
// @description  Get a better understanding of how you have performed across different contests, by getting a tabular view
// @author       Prakash
// @match        https://leetcode.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=leetcode.com
// @grant        none
// @license      GNU GPLv3 
// ==/UserScript==

async function getUserName() {
    // Query for getting the user name
    const submissionDetailsQuery = {
        query:
        '\n    query globalData {\n  userStatus {\n    username\n  }\n}\n    ',
        operationName: 'globalData',
    };
    const options = {
        method: 'POST',
        headers: {
            cookie: document.cookie, // required to authorize the API request
            'content-type': 'application/json',
        },
        body: JSON.stringify(submissionDetailsQuery),
    };
    const username = await fetch('https://leetcode.com/graphql/', options)
    .then(res => res.json())
    .then(res => res.data.userStatus.username);

    return username;
}

async function getContestInfo(theusername) {
    // Query for getting the contest stats
    const submissionDetailsQuery = {
        query:
        '\n    query userContestRankingInfo($username: String!) {\n  userContestRanking(username: $username) {\n    attendedContestsCount\n    rating\n    globalRanking\n    totalParticipants\n    topPercentage\n    badge {\n      name\n    }\n  }\n  userContestRankingHistory(username: $username) {\n    attended\n    trendDirection\n    problemsSolved\n    totalProblems\n    finishTimeInSeconds\n    rating\n    ranking\n    contest {\n      title\n      startTime\n    }\n  }\n}\n    ',
        variables: { username: theusername },
        operationName: 'userContestRankingInfo',
    };
    const options = {
        method: 'POST',
        headers: {
            cookie: document.cookie, // required to authorize the API request
            'content-type': 'application/json',
        },
        body: JSON.stringify(submissionDetailsQuery),
    };
    const data = await fetch('https://leetcode.com/graphql/', options)
    .then(res => res.json())
    .then(res => res.data.userContestRankingHistory);

    return data
}

// Apply alternating row background colors
function alternatingRowBackground(table) {
    var rows = table.querySelectorAll('tr');
    for (var i = 0; i < rows.length; i++) {
        rows[i].classList.remove('even', 'odd');
        rows[i].classList.add(i % 2 === 0 ? 'even' : 'odd');
    }
}

// Function to create table
function createTable(data) {
    var table = document.createElement('table');
    table.id = 'leetCodeContestTable';
    table.classList.add('styled-table'); // Add a class for styling

    // Create table headers
    var headers = ['StartTime', 'Title', 'Ranking', 'Rating', 'ProblemsSolved', 'FinishTimeInSeconds'];
    var headerRow = document.createElement('tr');
    headerRow.innerHTML += '<th class="hidden">TimeSpan</th>';
    headers.forEach(function(header, index) {
        var th = document.createElement('th');
        th.textContent = header;
        th.dataset.sortable = true;
        th.dataset.columnIndex = index;
        th.addEventListener('click', function() {
            sortTable(table, index);
        });
        headerRow.appendChild(th);
    });
    table.appendChild(headerRow);

    // Populate table rows
    data.forEach(function(entry, index) {
        var row = document.createElement('tr');
        row.innerHTML += '<td class="hidden">' + entry.contest.startTime + '</td>';
        row.innerHTML += '<td>' + new Date(entry.contest.startTime * 1000).toLocaleString() + '</td>';
        row.innerHTML += '<td>' + entry.contest.title + '</td>';
        row.innerHTML += '<td>' + entry.ranking + '</td>';
        row.innerHTML += '<td>' + entry.rating + '</td>';
        row.innerHTML += '<td>' + entry.problemsSolved + '</td>';
        row.innerHTML += '<td>' + entry.finishTimeInSeconds + '</td>';

        table.appendChild(row);
    });

    alternatingRowBackground(table);

    // Add this table to top of page
    var navbarContainer = document.getElementById('navbar-container');
    navbarContainer.insertAdjacentElement('afterend', table);
}

// Function to sort table
function sortTable(table, columnIndex) {
    var rows = Array.from(table.rows).slice(1); // Exclude header row
    var isAscending = !table.querySelector('th[data-column-index="' + columnIndex + '"]').classList.contains('asc');
    rows.sort(function(row1, row2) {
        var value1 = row1.cells[columnIndex+1].textContent;
        var value2 = row2.cells[columnIndex+1].textContent;
        if (columnIndex === 0) {
            value1 = row1.cells[columnIndex].textContent;
            value2 = row2.cells[columnIndex].textContent;
        } else {
            value1 = parseFloat(value1) || value1;
            value2 = parseFloat(value2) || value2;
        }
        return (isAscending ? 1 : -1) * (value1 > value2 ? 1 : -1);
    });

    // Reorder rows in table
    while (table.rows.length > 1) {
        table.deleteRow(1);
    }
    rows.forEach(function(row) {
        table.appendChild(row);
    });

    // Remove sorting indicator from all headers
    table.querySelectorAll('th[data-sortable]').forEach(function(header) {
        header.classList.remove('asc', 'desc');
    });

    // Add sorting indicator to the clicked header
    table.querySelector('th[data-column-index="' + columnIndex + '"]').classList.toggle(isAscending ? 'asc' : 'desc', true);
    // Apply alternating background to rows
    alternatingRowBackground(table);
}

// Inject CSS styles into the document head
function addTableCSS(){
    document.head.innerHTML += `
    <style id='leetcodeContestTableStyle'>
      .styled-table {
      border-collapse: collapse;
      width: 100%;
      }

      .styled-table th, .styled-table td {
      padding: 8px;
      text-align: left;
      border-bottom: 1px solid #ddd;
      position: relative;
      }

      .styled-table th::after {
      content: '';
      position: absolute;
      top: 50%;
      right: 8px;
      transform: translateY(-50%);
      font-size: 12px;
      }

      .styled-table th.asc::after {
      content: '↑';
      }

      .styled-table th.desc::after {
      content: '↓';
      }

      .styled-table th {
      background-color: #f2f2f2;
      cursor: pointer;
      }

      .styled-table tr.even {
      background-color: #f9f9f9;
      }

      .styled-table tr.odd {
      background-color: #ffffff;
      }

      .hidden {
      display: none;
      }
    </style>
    `;
  }

function addSpinnerCSS(){
    document.head.innerHTML += `
    <style id="initial-loading-style">
      #initial-loading {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        background: white;
        transition: opacity .6s;
        z-index: 1;
      }

      #initial-loading[data-is-hide="true"] {
        opacity: 0;
        pointer-events: none;
      }

      #initial-loading .spinner {
        display: flex;
      }

      #initial-loading .bounce {
        width: 18px;
        height: 18px;
        margin: 0 3px;
        background-color: #999999;
        border-radius: 100%;
        display: inline-block;
        -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
        animation: sk-bouncedelay 1.4s infinite ease-in-out both;
      }

      #initial-loading .bounce:nth-child(1) {
        -webkit-animation-delay: -0.32s;
        animation-delay: -0.32s;
      }

      #initial-loading .bounce:nth-child(2) {
        -webkit-animation-delay: -0.16s;
        animation-delay: -0.16s;
      }

      @-webkit-keyframes sk-bouncedelay {

        0%,
        80%,
        100% {
          -webkit-transform: scale(0);
          transform: scale(0);
        }

        40% {
          -webkit-transform: scale(1.0);
          transform: scale(1.0);
        }
      }

      @keyframes sk-bouncedelay {

        0%,
        80%,
        100% {
          -webkit-transform: scale(0);
          transform: scale(0);
        }

        40% {
          -webkit-transform: scale(1.0);
          transform: scale(1.0);
        }
      }
    </style>
  `;
  }

function toggleSpinner(startSpinner){
    var initialLoadingDiv = document.getElementById('initial-loading');
    var initialLoadingStyle = document.getElementById('initial-loading-style');

    if (initialLoadingDiv && !startSpinner) {
        initialLoadingDiv.parentNode.removeChild(initialLoadingDiv);
        if (initialLoadingStyle) initialLoadingStyle.parentNode.removeChild(initialLoadingStyle);
    }
    else if(!initialLoadingDiv && startSpinner){
        // Create initial loading div
        var initialLoadingDiv1 = document.createElement('div');
        initialLoadingDiv1.id = 'initial-loading';

        // Create spinner div
        var spinnerDiv = document.createElement('div');
        spinnerDiv.className = 'spinner';

        // Create bounce divs inside spinner div
        for (var i = 0; i < 3; i++) {
            var bounceDiv = document.createElement('div');
            bounceDiv.className = 'bounce';
            spinnerDiv.appendChild(bounceDiv);
        }

        // Append spinner div to initial loading div
        initialLoadingDiv1.appendChild(spinnerDiv);

        // Append initial loading div to the document body
        document.body.appendChild(initialLoadingDiv1);
        addSpinnerCSS();
    }
}

function removeOldTable(){
    var oldTable = document.getElementById("leetCodeContestTable");
    var styleElement = document.getElementById("leetcodeContestTableStyle");
    if (oldTable){
        oldTable.parentNode.removeChild(oldTable);
        if (styleElement) styleElement.parentNode.removeChild(styleElement);
        return true;
    }
    return false;
}

async function execute(){
    // remove existing table if it exists
    if(removeOldTable()) return;

    toggleSpinner(true);
    try {
        // fetch contest details
        var theusername = await getUserName();
        var contestdata = await getContestInfo(theusername);
        var participatedContestData = contestdata.filter((entry) => entry.attended == true && entry.ranking != 0);

        // Create and append table to the document body
        addTableCSS();
        createTable(participatedContestData);
    } catch (error) {
        console.error("An error occurred:", error);
    } finally {
        toggleSpinner(false);
    }
}

execute();