Add Contest History Link

Add Contest History Link to pages under leetcode.com

  1. // ==UserScript==
  2. // @name Add Contest History Link
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description Add Contest History Link to pages under leetcode.com
  6. // @author V3L0CITY
  7. // @match *://leetcode.com/*
  8. // @license MIT
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // Function to add Contest History Link
  16. function addContestHistoryLink() {
  17. // Find all <a> elements on the page
  18. const allLinks = document.getElementsByTagName('a');
  19.  
  20. for (let i = 0; i < allLinks.length; i++) {
  21. const link = allLinks[i];
  22.  
  23. // Check if the text content of the <a> element is "Explore"
  24. if (link.textContent.trim() === "Contest History") {
  25. return;
  26. }
  27. }
  28. // Loop through all <a> elements
  29. for (let i = 0; i < allLinks.length; i++) {
  30. const link = allLinks[i];
  31.  
  32. // Check if the text content of the <a> element is "Explore"
  33. if (link.textContent.trim() === "Explore") {
  34. // Find the second parent (grandparent) of the <a> element
  35. const bigGuy = link.parentElement.parentElement;
  36.  
  37. if (bigGuy) {
  38. // Create a new list item element
  39. const listItem = document.createElement('li');
  40. listItem.className = 'relative flex h-full items-center text-sm';
  41.  
  42. // Create a new <a> element for Contest History
  43. const contestHistoryLink = document.createElement('a');
  44. 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';
  45. contestHistoryLink.textContent = 'Contest History';
  46.  
  47. // Add an event listener to the Contest History link
  48. contestHistoryLink.addEventListener('click', function() {
  49. // Execute your script here
  50. // Replace the following line with your script
  51. async function getUserName() {
  52. // Query for getting the user name
  53. const submissionDetailsQuery = {
  54. query:
  55. '\n query globalData {\n userStatus {\n username\n }\n}\n ',
  56. operationName: 'globalData',
  57. };
  58. const options = {
  59. method: 'POST',
  60. headers: {
  61. cookie: document.cookie, // required to authorize the API request
  62. 'content-type': 'application/json',
  63. },
  64. body: JSON.stringify(submissionDetailsQuery),
  65. };
  66. const username = await fetch('https://leetcode.com/graphql/', options)
  67. .then(res => res.json())
  68. .then(res => res.data.userStatus.username);
  69.  
  70. return username;
  71. }
  72.  
  73. async function getContestInfo(theusername) {
  74. // Query for getting the contest stats
  75. const submissionDetailsQuery = {
  76. query:
  77. '\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 ',
  78. variables: { username: theusername },
  79. operationName: 'userContestRankingInfo',
  80. };
  81. const options = {
  82. method: 'POST',
  83. headers: {
  84. cookie: document.cookie, // required to authorize the API request
  85. 'content-type': 'application/json',
  86. },
  87. body: JSON.stringify(submissionDetailsQuery),
  88. };
  89. const data = await fetch('https://leetcode.com/graphql/', options)
  90. .then(res => res.json())
  91. .then(res => res.data.userContestRankingHistory);
  92.  
  93. return data
  94. }
  95.  
  96. // Apply alternating row background colors
  97. function alternatingRowBackground(table) {
  98. var rows = table.querySelectorAll('tr');
  99. for (var i = 0; i < rows.length; i++) {
  100. rows[i].classList.remove('even', 'odd');
  101. rows[i].classList.add(i % 2 === 0 ? 'even' : 'odd');
  102. }
  103. }
  104.  
  105. // Function to create table
  106. function createTable(data) {
  107. var table = document.createElement('table');
  108. table.id = 'leetCodeContestTable';
  109. table.classList.add('styled-table'); // Add a class for styling
  110.  
  111. // Create table headers
  112. var headers = ['StartTime', 'Title', 'Ranking', 'Rating', 'ProblemsSolved', 'FinishTimeInSeconds'];
  113. var headerRow = document.createElement('tr');
  114. headerRow.innerHTML += '<th class="hidden">TimeSpan</th>';
  115. headers.forEach(function(header, index) {
  116. var th = document.createElement('th');
  117. th.textContent = header;
  118. th.dataset.sortable = true;
  119. th.dataset.columnIndex = index;
  120. th.addEventListener('click', function() {
  121. sortTable(table, index);
  122. });
  123. headerRow.appendChild(th);
  124. });
  125. table.appendChild(headerRow);
  126.  
  127. // Populate table rows
  128. data.forEach(function(entry, index) {
  129. var row = document.createElement('tr');
  130. row.innerHTML += '<td class="hidden">' + entry.contest.startTime + '</td>';
  131. row.innerHTML += '<td>' + new Date(entry.contest.startTime * 1000).toLocaleString() + '</td>';
  132. row.innerHTML += '<td>' + entry.contest.title + '</td>';
  133. row.innerHTML += '<td>' + entry.ranking + '</td>';
  134. row.innerHTML += '<td>' + entry.rating + '</td>';
  135. row.innerHTML += '<td>' + entry.problemsSolved + '</td>';
  136. row.innerHTML += '<td>' + entry.finishTimeInSeconds + '</td>';
  137.  
  138. table.appendChild(row);
  139. });
  140.  
  141. alternatingRowBackground(table);
  142.  
  143. // Add this table to top of page
  144. var navbarContainer = document.getElementById('navbar-container');
  145. navbarContainer.insertAdjacentElement('afterend', table);
  146. }
  147.  
  148. // Function to sort table
  149. function sortTable(table, columnIndex) {
  150. var rows = Array.from(table.rows).slice(1); // Exclude header row
  151. var isAscending = !table.querySelector('th[data-column-index="' + columnIndex + '"]').classList.contains('asc');
  152. rows.sort(function(row1, row2) {
  153. var value1 = row1.cells[columnIndex+1].textContent;
  154. var value2 = row2.cells[columnIndex+1].textContent;
  155. if (columnIndex === 0) {
  156. value1 = row1.cells[columnIndex].textContent;
  157. value2 = row2.cells[columnIndex].textContent;
  158. } else {
  159. value1 = parseFloat(value1) || value1;
  160. value2 = parseFloat(value2) || value2;
  161. }
  162. return (isAscending ? 1 : -1) * (value1 > value2 ? 1 : -1);
  163. });
  164.  
  165. // Reorder rows in table
  166. while (table.rows.length > 1) {
  167. table.deleteRow(1);
  168. }
  169. rows.forEach(function(row) {
  170. table.appendChild(row);
  171. });
  172.  
  173. // Remove sorting indicator from all headers
  174. table.querySelectorAll('th[data-sortable]').forEach(function(header) {
  175. header.classList.remove('asc', 'desc');
  176. });
  177.  
  178. // Add sorting indicator to the clicked header
  179. table.querySelector('th[data-column-index="' + columnIndex + '"]').classList.toggle(isAscending ? 'asc' : 'desc', true);
  180. // Apply alternating background to rows
  181. alternatingRowBackground(table);
  182. }
  183.  
  184. // Inject CSS styles into the document head
  185. function addTableCSS(){
  186. document.head.innerHTML += `
  187. <style id='leetcodeContestTableStyle'>
  188. .styled-table {
  189. border-collapse: collapse;
  190. width: 100%;
  191. }
  192.  
  193. .styled-table th, .styled-table td {
  194. padding: 8px;
  195. text-align: left;
  196. border-bottom: 1px solid #ddd;
  197. position: relative;
  198. }
  199.  
  200. .styled-table th::after {
  201. content: '';
  202. position: absolute;
  203. top: 50%;
  204. right: 8px;
  205. transform: translateY(-50%);
  206. font-size: 12px;
  207. }
  208.  
  209. .styled-table th.asc::after {
  210. content: '↑';
  211. }
  212.  
  213. .styled-table th.desc::after {
  214. content: '↓';
  215. }
  216.  
  217. .styled-table th {
  218. background-color: #f2f2f2;
  219. cursor: pointer;
  220. }
  221.  
  222. .styled-table tr.even {
  223. background-color: #f9f9f9;
  224. }
  225.  
  226. .styled-table tr.odd {
  227. background-color: #ffffff;
  228. }
  229.  
  230. .hidden {
  231. display: none;
  232. }
  233. </style>
  234. `;
  235. }
  236.  
  237. function addSpinnerCSS(){
  238. document.head.innerHTML += `
  239. <style id="initial-loading-style">
  240. #initial-loading {
  241. position: fixed;
  242. top: 0;
  243. left: 0;
  244. right: 0;
  245. bottom: 0;
  246. display: flex;
  247. align-items: center;
  248. justify-content: center;
  249. background: white;
  250. transition: opacity .6s;
  251. z-index: 1;
  252. }
  253.  
  254. #initial-loading[data-is-hide="true"] {
  255. opacity: 0;
  256. pointer-events: none;
  257. }
  258.  
  259. #initial-loading .spinner {
  260. display: flex;
  261. }
  262.  
  263. #initial-loading .bounce {
  264. width: 18px;
  265. height: 18px;
  266. margin: 0 3px;
  267. background-color: #999999;
  268. border-radius: 100%;
  269. display: inline-block;
  270. -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
  271. animation: sk-bouncedelay 1.4s infinite ease-in-out both;
  272. }
  273.  
  274. #initial-loading .bounce:nth-child(1) {
  275. -webkit-animation-delay: -0.32s;
  276. animation-delay: -0.32s;
  277. }
  278.  
  279. #initial-loading .bounce:nth-child(2) {
  280. -webkit-animation-delay: -0.16s;
  281. animation-delay: -0.16s;
  282. }
  283.  
  284. @-webkit-keyframes sk-bouncedelay {
  285.  
  286. 0%,
  287. 80%,
  288. 100% {
  289. -webkit-transform: scale(0);
  290. transform: scale(0);
  291. }
  292.  
  293. 40% {
  294. -webkit-transform: scale(1.0);
  295. transform: scale(1.0);
  296. }
  297. }
  298.  
  299. @keyframes sk-bouncedelay {
  300.  
  301. 0%,
  302. 80%,
  303. 100% {
  304. -webkit-transform: scale(0);
  305. transform: scale(0);
  306. }
  307.  
  308. 40% {
  309. -webkit-transform: scale(1.0);
  310. transform: scale(1.0);
  311. }
  312. }
  313. </style>
  314. `;
  315. }
  316.  
  317. function toggleSpinner(startSpinner){
  318. var initialLoadingDiv = document.getElementById('initial-loading');
  319. var initialLoadingStyle = document.getElementById('initial-loading-style');
  320.  
  321. if (initialLoadingDiv && !startSpinner) {
  322. initialLoadingDiv.parentNode.removeChild(initialLoadingDiv);
  323. if (initialLoadingStyle) initialLoadingStyle.parentNode.removeChild(initialLoadingStyle);
  324. }
  325. else if(!initialLoadingDiv && startSpinner){
  326. // Create initial loading div
  327. var newInitialLoadingDiv = document.createElement('div');
  328. newInitialLoadingDiv.id = 'initial-loading';
  329.  
  330. // Create spinner div
  331. var spinnerDiv = document.createElement('div');
  332. spinnerDiv.className = 'spinner';
  333.  
  334. // Create bounce divs inside spinner div
  335. for (var i = 0; i < 3; i++) {
  336. var bounceDiv = document.createElement('div');
  337. bounceDiv.className = 'bounce';
  338. spinnerDiv.appendChild(bounceDiv);
  339. }
  340.  
  341. // Append spinner div to initial loading div
  342. newInitialLoadingDiv.appendChild(spinnerDiv);
  343.  
  344. // Append initial loading div to the document body
  345. document.body.appendChild(newInitialLoadingDiv);
  346. addSpinnerCSS();
  347. }
  348. }
  349.  
  350. function removeOldTable(){
  351. var oldTable = document.getElementById("leetCodeContestTable");
  352. var styleElement = document.getElementById("leetcodeContestTableStyle");
  353. if (oldTable){
  354. oldTable.parentNode.removeChild(oldTable);
  355. if (styleElement) styleElement.parentNode.removeChild(styleElement);
  356. return true;
  357. }
  358. return false;
  359. }
  360.  
  361. async function execute(){
  362. // remove existing table if it exists
  363. if(removeOldTable()) return;
  364.  
  365. toggleSpinner(true);
  366. try {
  367. // fetch contest details
  368. var theusername = await getUserName();
  369. var contestdata = await getContestInfo(theusername);
  370. var participatedContestData = contestdata.filter((entry) => entry.attended == true && entry.ranking != 0)
  371.  
  372. // Create and append table to the document body
  373. addTableCSS();
  374. createTable(participatedContestData);
  375. } catch (error) {
  376. console.error("An error occurred:", error);
  377. } finally {
  378. toggleSpinner(false);
  379. }
  380. }
  381.  
  382. execute();
  383. });
  384.  
  385. // Append the Contest History link to the list item
  386. listItem.appendChild(contestHistoryLink);
  387.  
  388. // Append the list item to the big guy
  389. bigGuy.appendChild(listItem);
  390.  
  391. // Break the loop since we found the first <a> element with the text "Explore"
  392. break;
  393. }
  394. }
  395. }
  396. }
  397.  
  398. // Call the function initially
  399. addContestHistoryLink();
  400.  
  401. // Create a MutationObserver instance
  402. const observer = new MutationObserver(addContestHistoryLink);
  403.  
  404. // Observe changes to the document body
  405. observer.observe(document.body, { childList: true, subtree: true });
  406. })();