leetcode2notion

Save LeetCode problems to Notion after clicking a button.

  1. // ==UserScript==
  2. // @name leetcode2notion
  3. // @namespace wuyifff
  4. // @version 1.2
  5. // @description Save LeetCode problems to Notion after clicking a button.
  6. // @author wuyifff
  7. // @match https://leetcode.cn/problems/*
  8. // @match https://leetcode.com/problems/*
  9. // @connect api.notion.com
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=leetcode.com
  11. // @grant GM_xmlhttpRequest
  12. // @license MIT
  13. // @homepage https://github.com/wuyifff/leetcode2notion
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18. // replace to your own token and ID
  19. const notionToken = ''; // Notion API token
  20. const databaseId = ''; // Notion database ID
  21.  
  22. // 1. add save button
  23. // select language button (optional)
  24. let currentMinutes = 0;
  25. let currentSeconds = 0;
  26. function addUIElements() {
  27. // 1.1 save button
  28. const button = document.createElement("button");
  29. button.innerHTML = "Save to Notion";
  30. button.style.position = "fixed";
  31. button.style.bottom = "10px";
  32. button.style.right = "10px";
  33. button.style.zIndex = 1000;
  34. button.style.padding = "10px 20px";
  35. button.style.backgroundColor = "#4CAF50";
  36. button.style.color = "white";
  37. button.style.border = "none";
  38. button.style.borderRadius = "5px";
  39. button.style.cursor = "pointer";
  40. button.onclick = saveProblemToNotion;
  41.  
  42. // 1.2 save language button (disabled)
  43. const select = document.createElement("select");
  44. select.id = "languageSelect";
  45. select.style.position = "fixed";
  46. select.style.bottom = "50px";
  47. select.style.right = "10px";
  48. select.style.zIndex = 1000;
  49. select.style.padding = "10px";
  50. select.style.backgroundColor = "#4CAF50";
  51. select.style.color = "white";
  52. select.style.border = "none";
  53. select.style.borderRadius = "5px";
  54. select.style.cursor = "pointer";
  55. const optionPython = document.createElement("option");
  56. optionPython.value = "python";
  57. optionPython.innerText = "Python";
  58. const optionCpp = document.createElement("option");
  59. optionCpp.value = "cpp";
  60. optionCpp.innerText = "C++";
  61. select.appendChild(optionPython);
  62. select.appendChild(optionCpp);
  63.  
  64. const container = document.createElement("div");
  65. container.id = "save"
  66. container.style.display = "flex";
  67. container.style.flexDirection = "column";
  68. container.style.alignItems = "center";
  69. container.style.marginLeft = "10px";
  70. //container.appendChild(select);
  71. container.appendChild(button);
  72. container.style.position = "fixed";
  73. container.style.bottom = "10px";
  74. container.style.right = "10px";
  75. document.body.appendChild(container);
  76. }
  77. function addTimer() {
  78. // Create timer span if it doesn't exist
  79. let timerSpan = document.querySelector('#timerSpan');
  80. if (!timerSpan) {
  81. timerSpan = document.createElement("span");
  82. timerSpan.id = "timerSpan";
  83. timerSpan.className = 'ml-2 group/nav-back cursor-pointer gap-2 hover:text-lc-icon-primary dark:hover:text-dark-lc-icon-primary flex items-center h-[32px] transition-none hover:bg-fill-quaternary dark:hover:bg-fill-quaternary text-gray-60 dark:text-gray-60 px-2';
  84.  
  85. // Append the timer span to the target location
  86. const targetDiv = document.getElementById('ide-top-btns');
  87. if (targetDiv) {
  88. targetDiv.appendChild(timerSpan);
  89. console.log("append timer success");
  90. } else {
  91. console.log("no ide-top-btns element!");
  92. }
  93. }
  94. }
  95.  
  96. function updateTimer() {
  97. const now = new Date().getTime(); // Get the current time
  98. const elapsedTime = now - startTime; // Calculate the elapsed milliseconds
  99. const totalSeconds = Math.floor(elapsedTime / 1000); // Convert to seconds
  100. currentMinutes = Math.floor(totalSeconds / 60);
  101. currentSeconds = totalSeconds % 60;
  102. const formattedMinutes = currentMinutes < 10 ? `0${currentMinutes}` : currentMinutes;
  103. const formattedSeconds = currentSeconds < 10 ? `0${currentSeconds}` : currentSeconds;
  104.  
  105. // Make sure timerSpan is available
  106. let timerSpan = document.querySelector('#timerSpan');
  107. if (!timerSpan) {
  108. addTimer();
  109. timerSpan = document.querySelector('#timerSpan'); // Re-select after creation
  110. }
  111.  
  112. // Update the timer content
  113. timerSpan.textContent = `Time: ${formattedMinutes}:${formattedSeconds}`;
  114. }
  115.  
  116. // 2. get leetcode problem info
  117. function getProblemData() {
  118. const title = document.querySelector('.text-title-large a')?.innerText || 'No title found';
  119. const difficultyElement = document.querySelector("div[class*='text-difficulty-']");
  120. const difficulty = difficultyElement ? difficultyElement.innerText : 'No difficulty found';
  121. const url = window.location.href;
  122. const tagElements = document.querySelectorAll("a[href*='/tag/']");
  123. const tagTexts = Array.from(tagElements).map(element => element.innerText);
  124.  
  125. const codeDiv = document.querySelector('.view-lines.monaco-mouse-cursor-text[role="presentation"]');
  126. let codeText = '';
  127. if (codeDiv) {
  128. const codeLines = codeDiv.querySelectorAll('div');
  129. codeText = Array.from(codeLines).map(line => line.innerText).join('\n');
  130. } else {
  131. codeText = 'No code found';
  132. }
  133. //console.log(codeText);
  134. //const selectedLanguage = document.getElementById("languageSelect").value;
  135. const selectedLanguage = 'python';
  136. return {
  137. title: title,
  138. difficulty: difficulty,
  139. url: url,
  140. tag: tagTexts,
  141. code: codeText,
  142. language: selectedLanguage,
  143. time: currentMinutes
  144. };
  145. }
  146.  
  147. // 3. save to notion and check if duplicate
  148. async function saveProblemToNotion() {
  149. const problemData = getProblemData();
  150. console.log(problemData);
  151.  
  152. const searchUrl = `https://api.notion.com/v1/search`;
  153. const searchBody = {
  154. "query": problemData.title,
  155. "filter": {
  156. "value": "page",
  157. "property": "object"
  158. },
  159. "sort": {
  160. "direction": "ascending",
  161. "timestamp": "last_edited_time"
  162. }
  163. };
  164.  
  165. GM_xmlhttpRequest({
  166. method: 'POST',
  167. url: searchUrl,
  168. headers: {
  169. 'Authorization': `Bearer ${notionToken}`,
  170. 'Content-Type': 'application/json',
  171. 'Notion-Version': '2022-06-28'
  172. },
  173. data: JSON.stringify(searchBody),
  174. onload: function(searchResponse) {
  175. if (searchResponse.status === 200) {
  176. const searchResult = JSON.parse(searchResponse.responseText);
  177. const existingPage = searchResult.results.find(result => result.properties?.Title?.title[0]?.text?.content === problemData.title);
  178.  
  179. if (existingPage) {
  180. const existingPageUrl = existingPage.url;
  181. alert('Problem already exists in Notion! Opening existing page...');
  182. window.open(existingPageUrl, '_blank');
  183. } else {
  184. createNewNotionPage(problemData);
  185. }
  186. } else {
  187. console.error('Error searching Notion database', searchResponse.responseText);
  188. alert('Failed to search Notion database. Check the console for details.');
  189. }
  190. },
  191. onerror: function(error) {
  192. console.error('Error in searching Notion database', error);
  193. alert('An error occurred while searching Notion database.');
  194. }
  195. });
  196. }
  197.  
  198. // 4. create new page
  199. function createNewNotionPage(problemData) {
  200. const tags = problemData.tag.map(tag => ({
  201. name: tag
  202. }));
  203.  
  204. const url = `https://api.notion.com/v1/pages`;
  205. const body = {
  206. parent: { database_id: databaseId },
  207. properties: {
  208. 'Title': {
  209. title: [
  210. {
  211. text: {
  212. content: problemData.title
  213. }
  214. }
  215. ]
  216. },
  217. 'Difficulty': {
  218. select: {
  219. name: problemData.difficulty
  220. }
  221. },
  222. 'Link': {
  223. url: problemData.url
  224. },
  225. 'Date': {
  226. date: {
  227. start: new Date().toISOString().split('T')[0] // format YYYY-MM-DD
  228. }
  229. },
  230. 'Tags': {
  231. multi_select: tags
  232. },
  233. 'Time': {
  234. number: problemData.time
  235. },
  236. },
  237. children: [
  238. {
  239. object: 'block',
  240. type: 'code',
  241. code: {
  242. rich_text: [
  243. {
  244. type: 'text',
  245. text: {
  246. content: problemData.code
  247. }
  248. }
  249. ],
  250. language: problemData.language
  251. }
  252. }
  253. ]
  254. };
  255.  
  256. GM_xmlhttpRequest({
  257. method: 'POST',
  258. url: url,
  259. headers: {
  260. 'Authorization': `Bearer ${notionToken}`,
  261. 'Content-Type': 'application/json',
  262. 'Notion-Version': '2022-06-28'
  263. },
  264. data: JSON.stringify(body),
  265. onload: function(response) {
  266. if (response.status === 200) {
  267. const responseData = JSON.parse(response.responseText);
  268. const notionPageUrl = responseData.url;
  269. alert('Problem saved to Notion!');
  270. window.open(notionPageUrl, '_blank');
  271. } else {
  272. console.error('Failed to save to Notion', response.responseText);
  273. alert('Failed to save to Notion. Check the console for more details.');
  274. }
  275. },
  276. onerror: function(error) {
  277. console.error('Error in saving to Notion', error);
  278. alert('An error occurred while saving to Notion.');
  279. }
  280. });
  281. }
  282.  
  283. addUIElements();
  284. let startTime; // Record the start time
  285. setTimeout(function() {
  286. startTime = new Date().getTime();
  287. var tmp = setInterval(updateTimer, 1000); // update every second
  288. }, 5000); // delay 5 seconds
  289. })();