├── .github └── workflows │ └── schedule.yml ├── .gitignore ├── LICENSE ├── README.md ├── action.yml ├── assets └── sample.png ├── index.js ├── leetcode.js ├── package-lock.json └── package.json /.github/workflows/schedule.yml: -------------------------------------------------------------------------------- 1 | name: Update LeetCode gist 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' 5 | push: 6 | branches: main 7 | 8 | jobs: 9 | update-leetcode-gist: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - run: npm install 14 | - name: Update LeetCode Gist 15 | uses: ./ 16 | env: 17 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 18 | GIST_ID: ${{ secrets.GIST_ID }} 19 | LEETCODE_USERNAME: ${{ secrets.LEETCODE_USERNAME }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | target/ 3 | 4 | ### VS Code ### 5 | .vscode/ 6 | 7 | ### MacOS ### 8 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Patrick Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

leetcode-box

4 |

Update a pinned gist to show your LeetCode stats.

5 |

6 | 7 | ![](https://img.shields.io/github/license/puiiyuen/leetcode-box) 8 | ![](https://img.shields.io/github/workflow/status/puiiyuen/leetcode-box/Update%20LeetCode%20gist) 9 | --- 10 | > 📌✨ For more pinned-gist projects like this one, check out: 11 | > https://github.com/matchai/awesome-pinned-gists 12 | 13 | ## Prep Work 14 | 15 | 1. Create a new public GitHub Gist (https://gist.github.com/) 16 | 2. Create a token with the `gist` scope and copy it. (https://github.com/settings/tokens/new) 17 | 3. Find the username from your LeetCode profile `https://leetcode.com/{username}` 18 | 19 | ## Project Setup 20 | 21 | 1. Fork this repository 22 | 2. Go to your fork's `Settings` > `Secrets` > `Add a new secret` for each environment secret: 23 | - **`GIST_ID`:** 32 bits ID from your gist URL 24 | `https://gist.github.com/{github_username}/`**`762ebda9730630395aabdee06ce58fd1`** 25 | - **`GH_TOKEN`:** The GitHub token generated above 26 | - **`LEETCODE_USERNAME`:** Your LeetCode Username 27 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "leetcode-box" 2 | author: puiiyuen 3 | description: "Update a pinned gist to show your LeetCode stats" 4 | branding: 5 | icon: code 6 | color: blue 7 | runs: 8 | using: "node12" 9 | main: "./index.js" -------------------------------------------------------------------------------- /assets/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puiiyuen/leetcode-box/b2e0c024627fe769089e8de321b34e55e4a24ef1/assets/sample.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | const { getLeetCodeStats } = require('./leetcode') 4 | const { Octokit } = require('@octokit/rest') 5 | 6 | const { 7 | GH_TOKEN: github_token, 8 | GIST_ID: gist_id 9 | } = process.env 10 | 11 | const octokit = new Octokit({ 12 | auth: `token ${github_token}` 13 | }) 14 | 15 | async function main() { 16 | const leetcode = await getLeetCodeStats() 17 | await updateLeetCodeGist(leetcode) 18 | } 19 | 20 | async function updateLeetCodeGist(leetcode) { 21 | let gist 22 | try { 23 | gist = await octokit.gists.get({ 24 | gist_id 25 | }) 26 | } catch (error) { 27 | console.error( 28 | `leetcode-box cannot resolve your gist:\n${error}` 29 | ) 30 | } 31 | 32 | const lines = [] 33 | 34 | const title = [ 35 | "Difficulty".padEnd(10), 36 | "Solved".padEnd(9), 37 | "Accepted Rate".padEnd(8) 38 | ] 39 | lines.push(title.join(" ")) 40 | 41 | for (let i = 0; i < leetcode.solved.length; i++) { 42 | const difficulty = leetcode.solved[i].difficulty 43 | const acceptedRate = leetcode.solved[i].acceptedRate 44 | const solvedRadio = leetcode.solved[i].solvedRadio 45 | 46 | const line = [ 47 | difficulty.padEnd(10), 48 | solvedRadio.padEnd(9), 49 | generateBarChart(acceptedRate, 20), 50 | String(acceptedRate.toFixed(1)).padStart(5) + "%" 51 | ] 52 | lines.push(line.join(" ")) 53 | } 54 | 55 | 56 | try { 57 | const filename = Object.keys(gist.data.files)[0] 58 | await octokit.gists.update({ 59 | gist_id: gist_id, 60 | files: { 61 | [filename]: { 62 | filename: `💻 My LeetCode Stats ✨`, 63 | content: lines.join("\n") 64 | } 65 | } 66 | }) 67 | } catch (error) { 68 | console.error(`Unable to update gist\n${error}`) 69 | } 70 | 71 | } 72 | 73 | function generateBarChart(percent, size) { 74 | const syms = "░▏▎▍▌▋▊▉█"; 75 | 76 | const frac = Math.floor((size * 8 * percent) / 100); 77 | const barsFull = Math.floor(frac / 8); 78 | if (barsFull >= size) { 79 | return syms.substring(8, 9).repeat(size); 80 | } 81 | const semi = frac % 8; 82 | 83 | return [syms.substring(8, 9).repeat(barsFull), syms.substring(semi, semi + 1)] 84 | .join("") 85 | .padEnd(size, syms.substring(0, 1)); 86 | } 87 | 88 | (async() => { 89 | await main() 90 | })() 91 | -------------------------------------------------------------------------------- /leetcode.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const axios = require('axios').default; 4 | 5 | const { 6 | LEETCODE_USERNAME: username 7 | } = process.env 8 | 9 | const leetcodeURL = 'https://leetcode.com/graphql' 10 | 11 | const getOriginalLeetCodeStats = async() => { 12 | const response = await axios.post(leetcodeURL, { 13 | query: ` 14 | query getUserProfile($username: String!) { 15 | allQuestionsCount { 16 | difficulty 17 | count 18 | } 19 | matchedUser(username: $username) { 20 | username 21 | submitStats: submitStatsGlobal { 22 | acSubmissionNum { 23 | difficulty 24 | count 25 | submissions 26 | } 27 | totalSubmissionNum { 28 | difficulty 29 | count 30 | submissions 31 | } 32 | } 33 | } 34 | }`, 35 | variables: { 36 | username: username 37 | } 38 | }) 39 | return response.data.data 40 | } 41 | 42 | exports.getLeetCodeStats = async function getLeetCodeStats() { 43 | const originStat = await getOriginalLeetCodeStats() 44 | const matchedUser = originStat.matchedUser 45 | let result = { 46 | "username": null, 47 | "solved": [] 48 | } 49 | if (matchedUser !== null) { 50 | result.username = matchedUser.username 51 | for (let i = 0; i < 4; i++) { 52 | const difficulty = matchedUser.submitStats.acSubmissionNum[i].difficulty 53 | const accepted = matchedUser.submitStats.acSubmissionNum[i].count 54 | const allQuestion = originStat.allQuestionsCount[i].count 55 | const acSubmission = matchedUser.submitStats.acSubmissionNum[i].submissions 56 | const totalSubmission = matchedUser.submitStats.totalSubmissionNum[i].submissions 57 | let question = { 58 | "difficulty": difficulty, 59 | "acceptedRate": totalSubmission == 0 ? 0.0 : (acSubmission / totalSubmission * 100), 60 | "solvedRadio": accepted + "/" + allQuestion 61 | } 62 | result.solved.push(question) 63 | } 64 | } 65 | return result; 66 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leetcode-box", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@octokit/auth-token": { 8 | "version": "2.5.0", 9 | "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", 10 | "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", 11 | "requires": { 12 | "@octokit/types": "^6.0.3" 13 | } 14 | }, 15 | "@octokit/core": { 16 | "version": "3.5.1", 17 | "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz", 18 | "integrity": "sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==", 19 | "requires": { 20 | "@octokit/auth-token": "^2.4.4", 21 | "@octokit/graphql": "^4.5.8", 22 | "@octokit/request": "^5.6.0", 23 | "@octokit/request-error": "^2.0.5", 24 | "@octokit/types": "^6.0.3", 25 | "before-after-hook": "^2.2.0", 26 | "universal-user-agent": "^6.0.0" 27 | } 28 | }, 29 | "@octokit/endpoint": { 30 | "version": "6.0.12", 31 | "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", 32 | "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", 33 | "requires": { 34 | "@octokit/types": "^6.0.3", 35 | "is-plain-object": "^5.0.0", 36 | "universal-user-agent": "^6.0.0" 37 | } 38 | }, 39 | "@octokit/graphql": { 40 | "version": "4.8.0", 41 | "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", 42 | "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", 43 | "requires": { 44 | "@octokit/request": "^5.6.0", 45 | "@octokit/types": "^6.0.3", 46 | "universal-user-agent": "^6.0.0" 47 | } 48 | }, 49 | "@octokit/openapi-types": { 50 | "version": "11.2.0", 51 | "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz", 52 | "integrity": "sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA==" 53 | }, 54 | "@octokit/plugin-paginate-rest": { 55 | "version": "2.17.0", 56 | "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz", 57 | "integrity": "sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw==", 58 | "requires": { 59 | "@octokit/types": "^6.34.0" 60 | } 61 | }, 62 | "@octokit/plugin-request-log": { 63 | "version": "1.0.4", 64 | "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", 65 | "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==" 66 | }, 67 | "@octokit/plugin-rest-endpoint-methods": { 68 | "version": "5.13.0", 69 | "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz", 70 | "integrity": "sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA==", 71 | "requires": { 72 | "@octokit/types": "^6.34.0", 73 | "deprecation": "^2.3.1" 74 | } 75 | }, 76 | "@octokit/request": { 77 | "version": "5.6.2", 78 | "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.2.tgz", 79 | "integrity": "sha512-je66CvSEVf0jCpRISxkUcCa0UkxmFs6eGDRSbfJtAVwbLH5ceqF+YEyC8lj8ystKyZTy8adWr0qmkY52EfOeLA==", 80 | "requires": { 81 | "@octokit/endpoint": "^6.0.1", 82 | "@octokit/request-error": "^2.1.0", 83 | "@octokit/types": "^6.16.1", 84 | "is-plain-object": "^5.0.0", 85 | "node-fetch": "^2.6.1", 86 | "universal-user-agent": "^6.0.0" 87 | } 88 | }, 89 | "@octokit/request-error": { 90 | "version": "2.1.0", 91 | "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", 92 | "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", 93 | "requires": { 94 | "@octokit/types": "^6.0.3", 95 | "deprecation": "^2.0.0", 96 | "once": "^1.4.0" 97 | } 98 | }, 99 | "@octokit/rest": { 100 | "version": "18.12.0", 101 | "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz", 102 | "integrity": "sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==", 103 | "requires": { 104 | "@octokit/core": "^3.5.1", 105 | "@octokit/plugin-paginate-rest": "^2.16.8", 106 | "@octokit/plugin-request-log": "^1.0.4", 107 | "@octokit/plugin-rest-endpoint-methods": "^5.12.0" 108 | } 109 | }, 110 | "@octokit/types": { 111 | "version": "6.34.0", 112 | "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz", 113 | "integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==", 114 | "requires": { 115 | "@octokit/openapi-types": "^11.2.0" 116 | } 117 | }, 118 | "axios": { 119 | "version": "0.24.0", 120 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", 121 | "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", 122 | "requires": { 123 | "follow-redirects": "^1.14.4" 124 | } 125 | }, 126 | "before-after-hook": { 127 | "version": "2.2.2", 128 | "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", 129 | "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==" 130 | }, 131 | "deprecation": { 132 | "version": "2.3.1", 133 | "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", 134 | "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" 135 | }, 136 | "dotenv": { 137 | "version": "10.0.0", 138 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", 139 | "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" 140 | }, 141 | "follow-redirects": { 142 | "version": "1.14.5", 143 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz", 144 | "integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==" 145 | }, 146 | "is-plain-object": { 147 | "version": "5.0.0", 148 | "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", 149 | "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" 150 | }, 151 | "node-fetch": { 152 | "version": "2.6.6", 153 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", 154 | "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", 155 | "requires": { 156 | "whatwg-url": "^5.0.0" 157 | } 158 | }, 159 | "once": { 160 | "version": "1.4.0", 161 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 162 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 163 | "requires": { 164 | "wrappy": "1" 165 | } 166 | }, 167 | "tr46": { 168 | "version": "0.0.3", 169 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 170 | "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" 171 | }, 172 | "universal-user-agent": { 173 | "version": "6.0.0", 174 | "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", 175 | "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" 176 | }, 177 | "webidl-conversions": { 178 | "version": "3.0.1", 179 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 180 | "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" 181 | }, 182 | "whatwg-url": { 183 | "version": "5.0.0", 184 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 185 | "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", 186 | "requires": { 187 | "tr46": "~0.0.3", 188 | "webidl-conversions": "^3.0.0" 189 | } 190 | }, 191 | "wrappy": { 192 | "version": "1.0.2", 193 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 194 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leetcode-box", 3 | "version": "0.1.2", 4 | "description": "Update a gist to contain your leetcode stats", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "puiiyuen", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@octokit/rest": "^18.12.0", 13 | "axios": "^0.24.0", 14 | "dotenv": "^10.0.0" 15 | } 16 | } --------------------------------------------------------------------------------