Greasy Fork is available in English.

Leetcode contest table

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

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