Add Contest History Link

Add Contest History Link to pages under leetcode.com

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Add Contest History Link
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Add Contest History Link to pages under leetcode.com
// @author       V3L0CITY
// @match        *://leetcode.com/*
// @license MIT
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Function to add Contest History Link
    function addContestHistoryLink() {
        // Find all <a> elements on the page
        const allLinks = document.getElementsByTagName('a');

        for (let i = 0; i < allLinks.length; i++) {
            const link = allLinks[i];

            // Check if the text content of the <a> element is "Explore"
            if (link.textContent.trim() === "Contest History") {
                return;
            }
        }
        // Loop through all <a> elements
        for (let i = 0; i < allLinks.length; i++) {
            const link = allLinks[i];

            // Check if the text content of the <a> element is "Explore"
            if (link.textContent.trim() === "Explore") {
                // Find the second parent (grandparent) of the <a> element
                const bigGuy = link.parentElement.parentElement;

                if (bigGuy) {
                    // Create a new list item element
                    const listItem = document.createElement('li');
                    listItem.className = 'relative flex h-full items-center text-sm';

                    // Create a new <a> element for Contest History
                    const contestHistoryLink = document.createElement('a');
                    contestHistoryLink.className = 'relative whitespace-nowrap hover:text-text-primary dark:hover:text-text-primary flex items-center text-base leading-[22px] cursor-pointer hover:text-text-primary dark:hover:text-text-primary text-text-secondary dark:text-text-secondary';
                    contestHistoryLink.textContent = 'Contest History';

                    // Add an event listener to the Contest History link
                    contestHistoryLink.addEventListener('click', function() {
                        // Execute your script here
                        // Replace the following line with your script
                        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  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 newInitialLoadingDiv = document.createElement('div');
                                newInitialLoadingDiv.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
                                newInitialLoadingDiv.appendChild(spinnerDiv);

                                // Append initial loading div to the document body
                                document.body.appendChild(newInitialLoadingDiv);
                                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();
                    });

                    // Append the Contest History link to the list item
                    listItem.appendChild(contestHistoryLink);

                    // Append the list item to the big guy
                    bigGuy.appendChild(listItem);

                    // Break the loop since we found the first <a> element with the text "Explore"
                    break;
                }
            }
        }
    }

    // Call the function initially
    addContestHistoryLink();

    // Create a MutationObserver instance
    const observer = new MutationObserver(addContestHistoryLink);

    // Observe changes to the document body
    observer.observe(document.body, { childList: true, subtree: true });
})();