From 6059cb16915898fab65ca82b062ae08eb31f4f3e Mon Sep 17 00:00:00 2001 From: anuraghazra Date: Tue, 14 Jul 2020 20:04:36 +0530 Subject: [PATCH 01/10] feat: added animations! --- src/getStyles.js | 97 +++++++++++++++++++++++++++++++++++ src/renderStatsCard.js | 52 +++++++++---------- tests/renderStatsCard.test.js | 5 +- 3 files changed, 123 insertions(+), 31 deletions(-) create mode 100644 src/getStyles.js diff --git a/src/getStyles.js b/src/getStyles.js new file mode 100644 index 0000000..198344f --- /dev/null +++ b/src/getStyles.js @@ -0,0 +1,97 @@ +const calculateCircleProgress = (value) => { + let radius = 40; + let c = Math.PI * (radius * 2); + + if (value < 0) value = 0; + if (value > 100) value = 100; + + let percentage = ((100 - value) / 100) * c; + return percentage; +}; + +const getAnimations = ({ progress }) => { + return ` + /* Animations */ + @keyframes scaleIn { + from { + transform: translate(-5px, 5px) scale(0); + } + to { + transform: translate(-5px, 5px) scale(1); + } + } + @keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + @keyframes rankAnimation { + from { + stroke-dashoffset: ${calculateCircleProgress(0)}; + } + to { + stroke-dashoffset: ${calculateCircleProgress(progress)}; + } + } + `; +}; + +const getStyles = ({ + titleColor, + textColor, + iconColor, + show_icons, + progress, +}) => { + return ` + .header { + font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor}; + animation: fadeIn 0.8s ease-in-out forwards; + } + .stat { + font: 600 14px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor}; + } + .stagger { + opacity: 0; + animation: fadeIn 0.3s ease-in-out forwards; + } + .rank-text { + font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor}; + animation: scaleIn 0.3s ease-in-out forwards; + } + + .bold { font-weight: 700 } + .star-icon { + font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; + } + .icon { + fill: ${iconColor}; + display: ${!!show_icons ? "block" : "none"}; + } + + .rank-circle-rim { + stroke: ${titleColor}; + fill: none; + stroke-width: 6; + opacity: 0.2; + } + .rank-circle { + stroke: ${titleColor}; + stroke-dasharray: 250; + fill: none; + stroke-width: 6; + stroke-linecap: round; + opacity: 0.8; + transform-origin: -10px 8px; + transform: rotate(-90deg); + animation: rankAnimation 1s forwards ease-in-out; + } + + ${process.env.NODE_ENV === "test" ? "" : getAnimations({ progress })} + `; +}; + +module.exports = getStyles; diff --git a/src/renderStatsCard.js b/src/renderStatsCard.js index af2951f..aa14653 100644 --- a/src/renderStatsCard.js +++ b/src/renderStatsCard.js @@ -1,12 +1,15 @@ const { kFormatter, isValidHexColor } = require("../src/utils"); +const getStyles = require("./getStyles"); const createTextNode = ({ icon, label, value, id, index, lineHeight }) => { const classname = icon === "★" && "star-icon"; const kValue = kFormatter(value); + const staggerDelay = (index + 3) * 150; // manually calculating lineHeight based on index instead of using // to fix firefox layout bug + const lheight = lineHeight * (index + 1); return ` - + ${icon} ${label}: @@ -107,7 +110,6 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => { /> `; - const rankProgress = 180 + rank.score * 0.8; const rankCircle = hide_rank ? "" : ` ${rank.level} `; + // re-adjust circle progressbar's value until the ranking algo is improved + let progress = rank.score; + if (rank.score > 86) { + progress = (40 + rank.score) * 0.6; + } + if (rank.score < 40) { + progress = 40 + rank.score; + } + + const styles = getStyles({ + titleColor, + textColor, + iconColor, + show_icons, + progress, + }); + return ` + ${hide_border ? "" : border} ${rankCircle} diff --git a/tests/renderStatsCard.test.js b/tests/renderStatsCard.test.js index 39ee63c..6375f4b 100644 --- a/tests/renderStatsCard.test.js +++ b/tests/renderStatsCard.test.js @@ -12,7 +12,7 @@ describe("Test renderStatsCard", () => { totalIssues: 300, totalPRs: 400, contributedTo: 500, - rank: { level: "A+", score: 100 }, + rank: { level: "A+", score: 40 }, }; it("should render correctly", () => { @@ -66,7 +66,8 @@ describe("Test renderStatsCard", () => { document.body.innerHTML = renderStatsCard(stats); const styleTag = document.querySelector("style"); - const stylesObject = cssToObject(styleTag.innerHTML); + console.log(styleTag.textContent); + const stylesObject = cssToObject(styleTag.textContent); const headerClassStyles = stylesObject[".header"]; const statClassStyles = stylesObject[".stat"]; From e3519ceb4c060d2cd13de47b5e996c3d595e3dcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Tue, 14 Jul 2020 21:30:03 +0200 Subject: [PATCH 02/10] Add documentation for Vercel setup --- readme.md | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index 008ba77..ee8291d 100644 --- a/readme.md +++ b/readme.md @@ -140,15 +140,29 @@ You usually won't be able to layout the images side by side to do that you can u ``` -## Deploy on your own vercel instance +## Deploy on your own Vercel instance Since Github API only allows 5k requests per hour it is possible that my `https://github-readme-stats.vercel.app/api` could hit the rate limiter thats why if you want to host it on your own vercel server then you don't have to worry about anything. click on the deploy button to get started [![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats) -Make sure to add your own `GITHUB_TOKEN` in `Environment Variables` -[Github Docs: Creating a github personal token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) +### Guide on setting up Vercel +1. Go to [vercel.com](https://vercel.com/) +1. Click on `Log in` +![](https://files.catbox.moe/tct1wg.png) +1. Sign in with GitHub by pressing `Continue with GitHub` +![](https://files.catbox.moe/btd78j.jpeg) +1. Sign into GitHub and allow access to all repositories, if prompted +1. Fork this repo +1. Go back to your [Vercel dashboard](https://vercel.com/dashboard) +1. Select `Import project` +![](https://files.catbox.moe/why170.png) +1. Select `Import Git Repository` +![](https://files.catbox.moe/pqub9q.png) +1. Select root and keep everything as is, just add your environment variable named GITHUB_TOKEN (as shown), which will contain a personal access token (PAT), which you can easily create [here](https://github.com/settings/tokens/new) (leave everything as is, just name it something, can be anything you want.) +![](https://files.catbox.moe/caem5b.png) +1. Click deploy, and you're good to go. See your domains to use the API! Contributions are welcomed! <3 -Made with :heart: and javascript. +Made with :heart: and JavaScript. From 3b3ce41ae42fd6919103f11f37bc6f620a83cfa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Tue, 14 Jul 2020 21:32:06 +0200 Subject: [PATCH 03/10] Fix img positions --- readme.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/readme.md b/readme.md index ee8291d..9ff9429 100644 --- a/readme.md +++ b/readme.md @@ -148,18 +148,18 @@ Since Github API only allows 5k requests per hour it is possible that my `https: ### Guide on setting up Vercel 1. Go to [vercel.com](https://vercel.com/) -1. Click on `Log in` +1. Click on `Log in` ![](https://files.catbox.moe/tct1wg.png) -1. Sign in with GitHub by pressing `Continue with GitHub` +1. Sign in with GitHub by pressing `Continue with GitHub` ![](https://files.catbox.moe/btd78j.jpeg) 1. Sign into GitHub and allow access to all repositories, if prompted 1. Fork this repo 1. Go back to your [Vercel dashboard](https://vercel.com/dashboard) -1. Select `Import project` +1. Select `Import project` ![](https://files.catbox.moe/why170.png) -1. Select `Import Git Repository` +1. Select `Import Git Repository` ![](https://files.catbox.moe/pqub9q.png) -1. Select root and keep everything as is, just add your environment variable named GITHUB_TOKEN (as shown), which will contain a personal access token (PAT), which you can easily create [here](https://github.com/settings/tokens/new) (leave everything as is, just name it something, can be anything you want.) +1. Select root and keep everything as is, just add your environment variable named GITHUB_TOKEN (as shown), which will contain a personal access token (PAT), which you can easily create [here](https://github.com/settings/tokens/new) (leave everything as is, just name it something, can be anything you want.) ![](https://files.catbox.moe/caem5b.png) 1. Click deploy, and you're good to go. See your domains to use the API! From dcde5f8205289605d17401b863686e89057876ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Tue, 14 Jul 2020 21:40:38 +0200 Subject: [PATCH 04/10] Crop the image a bit --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 9ff9429..6f3b97e 100644 --- a/readme.md +++ b/readme.md @@ -156,7 +156,7 @@ Since Github API only allows 5k requests per hour it is possible that my `https: 1. Fork this repo 1. Go back to your [Vercel dashboard](https://vercel.com/dashboard) 1. Select `Import project` -![](https://files.catbox.moe/why170.png) +![](https://files.catbox.moe/qckos0.png) 1. Select `Import Git Repository` ![](https://files.catbox.moe/pqub9q.png) 1. Select root and keep everything as is, just add your environment variable named GITHUB_TOKEN (as shown), which will contain a personal access token (PAT), which you can easily create [here](https://github.com/settings/tokens/new) (leave everything as is, just name it something, can be anything you want.) From 47370adbbbe451453a39dc4ee9720053a608b43a Mon Sep 17 00:00:00 2001 From: Micael Jarniac Date: Tue, 14 Jul 2020 18:08:52 -0300 Subject: [PATCH 05/10] Fixed typos, punctuation [...] Corrected many instances of "Github" to "GitHub", fixed the punctuation on some sections, fixed the ~lack of~ uppercase chars in others, tweaked the grammar a bit, and added the Vercel guide to a spoiler-ish section so it's only visible if you click to expand. <3 --- readme.md | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/readme.md b/readme.md index 6f3b97e..ad3a88f 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,5 @@

-

Github Readme Stats

+

GitHub Readme Stats

Get dynamically generated GitHub stats on your readmes!

@@ -25,26 +25,26 @@ # Features -- [Github Stats Card](#github-stats-card) -- [Github Extra Pins](#github-extra-pins) +- [GitHub Stats Card](#github-stats-card) +- [GitHub Extra Pins](#github-extra-pins) - [Customization](#customization) - [Deploy Yourself](#deploy-on-your-own-vercel-instance) -# Github Stats Card +# GitHub Stats Card Copy paste this into your markdown content, and that's it. Simple! -change the `?username=` value to your GitHubs's username +Change the `?username=` value to your GitHub's username. ```md [![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats) ``` -_Note: Ranks are calculated based on users stats, see [src/calculateRank.js](./src/calculateRank.js)_ +_Note: Ranks are calculated based on user's stats, see [src/calculateRank.js](./src/calculateRank.js)_ ### Hiding individual stats -To hide any specific stats, you can pass a query parameter `?hide=` with an array of items, you wanna hide. +To hide any specific stats, you can pass a query parameter `?hide=` with an array of items you wanna hide. > Options: `&hide=["stars","prs","issues","contribs"]` @@ -54,7 +54,7 @@ To hide any specific stats, you can pass a query parameter `?hide=` with an arra ### Showing icons -To enable icons, you can pass `show_icons=true` in the query param like so +To enable icons, you can pass `show_icons=true` in the query param, like so: ```md ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true) @@ -62,13 +62,13 @@ To enable icons, you can pass `show_icons=true` in the query param like so Other options: -- `&hide_border=true` hide the border box if you don't like it :D. -- `&line_height=30` control the line-height between text. +- `&hide_border=true` hide the border box if you don't like it :D +- `&line_height=30` control the line-height between text - `&hide_rank=true` hides the ranking ### Customization -You can customize the appearance of your `Stats Card` or `Repo Card` however you want with url params. +You can customize the appearance of your `Stats Card` or `Repo Card` however you want with URL params. Customization Options: @@ -107,11 +107,11 @@ Customization Options: ![Customized Card](https://github-readme-stats.vercel.app/api/pin?username=anuraghazra&repo=github-readme-stats&title_color=fff&icon_color=f9f9f9&text_color=9f9f9f&bg_color=151515) -# Github Extra Pins +# GitHub Extra Pins -Github extra pins allow you to pin more than 6 repositories in your profile using a GitHub readme profile. +GitHub extra pins allow you to pin more than 6 repositories in your profile using a GitHub readme profile. -Yey! you are no longer limited to 6 pinned repositories. +Yey! You are no longer limited to 6 pinned repositories. ### Usage @@ -129,7 +129,7 @@ Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats` ### Quick Tip (Align The Repo Cards) -You usually won't be able to layout the images side by side to do that you can use this approach +You usually won't be able to layout the images side by side. To do that you can use this approach: ```md @@ -142,11 +142,13 @@ You usually won't be able to layout the images side by side to do that you can u ## Deploy on your own Vercel instance -Since Github API only allows 5k requests per hour it is possible that my `https://github-readme-stats.vercel.app/api` could hit the rate limiter thats why if you want to host it on your own vercel server then you don't have to worry about anything. click on the deploy button to get started +Since the GitHub API only allows 5k requests per hour, it is possible that my `https://github-readme-stats.vercel.app/api` could hit the rate limiter. If you host it on your own Vercel server, then you don't have to worry about anything. Click on the deploy button to get started! [![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats) -### Guide on setting up Vercel +

+ Guide on setting up Vercel + 1. Go to [vercel.com](https://vercel.com/) 1. Click on `Log in` ![](https://files.catbox.moe/tct1wg.png) @@ -155,13 +157,14 @@ Since Github API only allows 5k requests per hour it is possible that my `https: 1. Sign into GitHub and allow access to all repositories, if prompted 1. Fork this repo 1. Go back to your [Vercel dashboard](https://vercel.com/dashboard) -1. Select `Import project` +1. Select `Import Project` ![](https://files.catbox.moe/qckos0.png) 1. Select `Import Git Repository` ![](https://files.catbox.moe/pqub9q.png) -1. Select root and keep everything as is, just add your environment variable named GITHUB_TOKEN (as shown), which will contain a personal access token (PAT), which you can easily create [here](https://github.com/settings/tokens/new) (leave everything as is, just name it something, can be anything you want.) +1. Select root and keep everything as is, just add your environment variable named GITHUB_TOKEN (as shown), which will contain a personal access token (PAT), which you can easily create [here](https://github.com/settings/tokens/new) (leave everything as is, just name it something, it can be anything you want) ![](https://files.catbox.moe/caem5b.png) 1. Click deploy, and you're good to go. See your domains to use the API! +
Contributions are welcomed! <3 From 3d8dea90e2072a5ce25aa148cf6ecb06aa7d123f Mon Sep 17 00:00:00 2001 From: anuraghazra Date: Wed, 15 Jul 2020 14:59:25 +0530 Subject: [PATCH 06/10] fix: github rate limiter with multiple PATs --- api/index.js | 2 ++ api/pin.js | 2 ++ src/fetchRepo.js | 17 ++++++---- src/fetchStats.js | 68 +++++++++++++++++++++++++++++++--------- src/utils.js | 12 +++++-- tests/fetchStats.test.js | 2 +- 6 files changed, 78 insertions(+), 25 deletions(-) diff --git a/api/index.js b/api/index.js index 392b694..803680e 100644 --- a/api/index.js +++ b/api/index.js @@ -18,7 +18,9 @@ module.exports = async (req, res) => { } = req.query; let stats; + res.setHeader("Cache-Control", "public, max-age=300"); res.setHeader("Content-Type", "image/svg+xml"); + try { stats = await fetchStats(username); } catch (err) { diff --git a/api/pin.js b/api/pin.js index 2d69c0e..a28733c 100644 --- a/api/pin.js +++ b/api/pin.js @@ -14,6 +14,8 @@ module.exports = async (req, res) => { } = req.query; let repoData; + + res.setHeader("Cache-Control", "public, max-age=300"); res.setHeader("Content-Type", "image/svg+xml"); try { diff --git a/src/fetchRepo.js b/src/fetchRepo.js index c7b7c3e..a248a7e 100644 --- a/src/fetchRepo.js +++ b/src/fetchRepo.js @@ -5,8 +5,9 @@ async function fetchRepo(username, reponame) { throw new Error("Invalid username or reponame"); } - const res = await request({ - query: ` + const res = await request( + { + query: ` fragment RepoInfo on Repository { name stargazers { @@ -33,11 +34,15 @@ async function fetchRepo(username, reponame) { } } `, - variables: { - login: username, - repo: reponame, + variables: { + login: username, + repo: reponame, + }, }, - }); + { + Authorization: `bearer ${process.env.PAT_1}`, + } + ); const data = res.data.data; diff --git a/src/fetchStats.js b/src/fetchStats.js index dc28590..a3cd3cd 100644 --- a/src/fetchStats.js +++ b/src/fetchStats.js @@ -2,25 +2,25 @@ const { request } = require("./utils"); const calculateRank = require("./calculateRank"); require("dotenv").config(); -async function fetchStats(username) { - if (!username) throw Error("Invalid username"); - - const res = await request({ - query: ` +// creating a fetcher function to reduce duplication +const fetcher = (username, token) => { + return request( + { + query: ` query userInfo($login: String!) { user(login: $login) { name login - repositoriesContributedTo(first: 100, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) { - totalCount - } contributionsCollection { totalCommitContributions } - pullRequests(first: 100) { + repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) { totalCount } - issues(first: 100) { + pullRequests(first: 1) { + totalCount + } + issues(first: 1) { totalCount } followers { @@ -36,9 +36,45 @@ async function fetchStats(username) { } } } - `, - variables: { login: username }, - }); + `, + variables: { login: username }, + }, + { + // set the token + Authorization: `bearer ${token}`, + } + ); +}; + +async function retryer(username, RETRIES) { + try { + console.log(`Trying PAT_${RETRIES + 1}`); + + // try to fetch with the first token since RETRIES is 0 index i'm adding +1 + let response = await fetcher(username, process.env[`PAT_${RETRIES + 1}`]); + + // if rate limit is hit increase the RETRIES and recursively call the retryer + // with username, and current RETRIES + if ( + response.data.errors && + response.data.errors[0].type === "RATE_LIMITED" + ) { + console.log(`PAT_${RETRIES} Failed`); + RETRIES++; + // directly return from the function + return await retryer(username, RETRIES); + } + + // finally return the response + return response; + } catch (err) { + console.log(err); + } +} + +async function fetchStats(username) { + let RETRIES = 0; + if (!username) throw Error("Invalid username"); const stats = { name: "", @@ -47,12 +83,14 @@ async function fetchStats(username) { totalIssues: 0, totalStars: 0, contributedTo: 0, - rank: "C", + rank: { level: "C", score: 0 }, }; + let res = await retryer(username, RETRIES); + if (res.data.errors) { console.log(res.data.errors); - throw Error("Could not fetch user"); + throw Error(res.data.errors[0].message || "Could not fetch user"); } const user = res.data.data.user; diff --git a/src/utils.js b/src/utils.js index 470a9e8..b6b74f5 100644 --- a/src/utils.js +++ b/src/utils.js @@ -33,13 +33,13 @@ function isValidHexColor(hexColor) { ).test(hexColor); } -function request(data) { +function request(data, headers) { return new Promise((resolve, reject) => { axios({ url: "https://api.github.com/graphql", method: "post", headers: { - Authorization: `bearer ${process.env.GITHUB_TOKEN}`, + ...headers, }, data, }) @@ -48,4 +48,10 @@ function request(data) { }); } -module.exports = { renderError, kFormatter, encodeHTML, isValidHexColor, request }; +module.exports = { + renderError, + kFormatter, + encodeHTML, + isValidHexColor, + request, +}; diff --git a/tests/fetchStats.test.js b/tests/fetchStats.test.js index 8ae45fa..ebcfde4 100644 --- a/tests/fetchStats.test.js +++ b/tests/fetchStats.test.js @@ -74,7 +74,7 @@ describe("Test fetchStats", () => { mock.onPost("https://api.github.com/graphql").reply(200, error); await expect(fetchStats("anuraghazra")).rejects.toThrow( - "Could not fetch user" + "Could not resolve to a User with the login of 'noname'." ); }); }); From 429d65a52bdd1618ca022b2577e1e080d1fe239a Mon Sep 17 00:00:00 2001 From: anuraghazra Date: Wed, 15 Jul 2020 17:46:31 +0530 Subject: [PATCH 07/10] refactor: refactored retryer logic & handled invalid tokens --- api/index.js | 2 +- api/pin.js | 2 +- src/fetchRepo.js | 24 +++++++++++---------- src/fetchStats.js | 36 ++++--------------------------- src/retryer.js | 43 +++++++++++++++++++++++++++++++++++++ tests/retryer.test.js | 50 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 112 insertions(+), 45 deletions(-) create mode 100644 src/retryer.js create mode 100644 tests/retryer.test.js diff --git a/api/index.js b/api/index.js index 803680e..94f734c 100644 --- a/api/index.js +++ b/api/index.js @@ -18,7 +18,7 @@ module.exports = async (req, res) => { } = req.query; let stats; - res.setHeader("Cache-Control", "public, max-age=300"); + res.setHeader("Cache-Control", "public, max-age=1800"); res.setHeader("Content-Type", "image/svg+xml"); try { diff --git a/api/pin.js b/api/pin.js index a28733c..8659c50 100644 --- a/api/pin.js +++ b/api/pin.js @@ -15,7 +15,7 @@ module.exports = async (req, res) => { let repoData; - res.setHeader("Cache-Control", "public, max-age=300"); + res.setHeader("Cache-Control", "public, max-age=1800"); res.setHeader("Content-Type", "image/svg+xml"); try { diff --git a/src/fetchRepo.js b/src/fetchRepo.js index a248a7e..710061e 100644 --- a/src/fetchRepo.js +++ b/src/fetchRepo.js @@ -1,11 +1,8 @@ const { request } = require("./utils"); +const retryer = require("./retryer"); -async function fetchRepo(username, reponame) { - if (!username || !reponame) { - throw new Error("Invalid username or reponame"); - } - - const res = await request( +const fetcher = (variables, token) => { + return request( { query: ` fragment RepoInfo on Repository { @@ -34,15 +31,20 @@ async function fetchRepo(username, reponame) { } } `, - variables: { - login: username, - repo: reponame, - }, + variables, }, { - Authorization: `bearer ${process.env.PAT_1}`, + Authorization: `bearer ${token}`, } ); +}; + +async function fetchRepo(username, reponame) { + if (!username || !reponame) { + throw new Error("Invalid username or reponame"); + } + + let res = await retryer(fetcher, { login: username, repo: reponame }); const data = res.data.data; diff --git a/src/fetchStats.js b/src/fetchStats.js index a3cd3cd..f8bb715 100644 --- a/src/fetchStats.js +++ b/src/fetchStats.js @@ -1,9 +1,9 @@ const { request } = require("./utils"); +const retryer = require("./retryer"); const calculateRank = require("./calculateRank"); require("dotenv").config(); -// creating a fetcher function to reduce duplication -const fetcher = (username, token) => { +const fetcher = (variables, token) => { return request( { query: ` @@ -37,43 +37,15 @@ const fetcher = (username, token) => { } } `, - variables: { login: username }, + variables, }, { - // set the token Authorization: `bearer ${token}`, } ); }; -async function retryer(username, RETRIES) { - try { - console.log(`Trying PAT_${RETRIES + 1}`); - - // try to fetch with the first token since RETRIES is 0 index i'm adding +1 - let response = await fetcher(username, process.env[`PAT_${RETRIES + 1}`]); - - // if rate limit is hit increase the RETRIES and recursively call the retryer - // with username, and current RETRIES - if ( - response.data.errors && - response.data.errors[0].type === "RATE_LIMITED" - ) { - console.log(`PAT_${RETRIES} Failed`); - RETRIES++; - // directly return from the function - return await retryer(username, RETRIES); - } - - // finally return the response - return response; - } catch (err) { - console.log(err); - } -} - async function fetchStats(username) { - let RETRIES = 0; if (!username) throw Error("Invalid username"); const stats = { @@ -86,7 +58,7 @@ async function fetchStats(username) { rank: { level: "C", score: 0 }, }; - let res = await retryer(username, RETRIES); + let res = await retryer(fetcher, { login: username }); if (res.data.errors) { console.log(res.data.errors); diff --git a/src/retryer.js b/src/retryer.js new file mode 100644 index 0000000..b62bd8a --- /dev/null +++ b/src/retryer.js @@ -0,0 +1,43 @@ +const retryer = async (fetcher, variables, retries = 0) => { + if (retries > 7) { + throw new Error("Maximum retries exceeded"); + } + try { + console.log(`Trying PAT_${retries + 1}`); + + // try to fetch with the first token since RETRIES is 0 index i'm adding +1 + let response = await fetcher( + variables, + process.env[`PAT_${retries + 1}`], + retries + ); + + // prettier-ignore + const isRateExceeded = response.data.errors && response.data.errors[0].type === "RATE_LIMITED"; + + // if rate limit is hit increase the RETRIES and recursively call the retryer + // with username, and current RETRIES + if (isRateExceeded) { + console.log(`PAT_${retries + 1} Failed`); + retries++; + // directly return from the function + return retryer(fetcher, variables, retries); + } + + // finally return the response + return response; + } catch (err) { + // prettier-ignore + // also checking for bad credentials if any tokens gets invalidated + const isBadCredential = err.response.data && err.response.data.message === "Bad credentials"; + + if (isBadCredential) { + console.log(`PAT_${retries + 1} Failed`); + retries++; + // directly return from the function + return retryer(fetcher, variables, retries); + } + } +}; + +module.exports = retryer; diff --git a/tests/retryer.test.js b/tests/retryer.test.js new file mode 100644 index 0000000..8b8a928 --- /dev/null +++ b/tests/retryer.test.js @@ -0,0 +1,50 @@ +require("@testing-library/jest-dom"); +const retryer = require("../src/retryer"); + +const fetcher = jest.fn((variables, token) => { + console.log(variables, token); + return new Promise((res, rej) => res({ data: "ok" })); +}); + +const fetcherFail = jest.fn(() => { + return new Promise((res, rej) => + res({ data: { errors: [{ type: "RATE_LIMITED" }] } }) + ); +}); + +const fetcherFailOnSecondTry = jest.fn((_vars, _token, retries) => { + return new Promise((res, rej) => { + // faking rate limit + if (retries < 1) { + return res({ data: { errors: [{ type: "RATE_LIMITED" }] } }); + } + return res({ data: "ok" }); + }); +}); + +describe("Test Retryer", () => { + it("retryer should return value and have zero retries on first try", async () => { + let res = await retryer(fetcher, {}); + + expect(fetcher).toBeCalledTimes(1); + expect(res).toStrictEqual({ data: "ok" }); + }); + + it("retryer should return value and have 2 retries", async () => { + let res = await retryer(fetcherFailOnSecondTry, {}); + + expect(fetcherFailOnSecondTry).toBeCalledTimes(2); + expect(res).toStrictEqual({ data: "ok" }); + }); + + it("retryer should throw error if maximum retries reached", async () => { + let res; + + try { + res = await retryer(fetcherFail, {}); + } catch (err) { + expect(fetcherFail).toBeCalledTimes(8); + expect(err.message).toBe("Maximum retries exceeded"); + } + }); +}); From 5fce46d67ccea01e49b24c5ea1e08bffc801ca0d Mon Sep 17 00:00:00 2001 From: anuraghazra Date: Wed, 15 Jul 2020 18:32:16 +0530 Subject: [PATCH 08/10] fix: query param booleans --- api/index.js | 8 ++++---- src/utils.js | 11 +++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/api/index.js b/api/index.js index 94f734c..6e25407 100644 --- a/api/index.js +++ b/api/index.js @@ -1,5 +1,5 @@ require("dotenv").config(); -const { renderError } = require("../src/utils"); +const { renderError, parseBoolean } = require("../src/utils"); const fetchStats = require("../src/fetchStats"); const renderStatsCard = require("../src/renderStatsCard"); @@ -30,9 +30,9 @@ module.exports = async (req, res) => { res.send( renderStatsCard(stats, { hide: JSON.parse(hide || "[]"), - show_icons, - hide_border, - hide_rank, + show_icons: parseBoolean(show_icons), + hide_border: parseBoolean(hide_border), + hide_rank: parseBoolean(hide_rank), line_height, title_color, icon_color, diff --git a/src/utils.js b/src/utils.js index b6b74f5..48b2811 100644 --- a/src/utils.js +++ b/src/utils.js @@ -33,6 +33,16 @@ function isValidHexColor(hexColor) { ).test(hexColor); } +function parseBoolean(value) { + if (value === "true") { + return true; + } else if (value === "false") { + return false; + } else { + return value; + } +} + function request(data, headers) { return new Promise((resolve, reject) => { axios({ @@ -54,4 +64,5 @@ module.exports = { encodeHTML, isValidHexColor, request, + parseBoolean, }; From 89bb8fea42ac1182305285708ba4f61c58529896 Mon Sep 17 00:00:00 2001 From: anuraghazra Date: Wed, 15 Jul 2020 18:59:38 +0530 Subject: [PATCH 09/10] design: fixed rank alignment --- src/renderStatsCard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderStatsCard.js b/src/renderStatsCard.js index aa14653..a4c6447 100644 --- a/src/renderStatsCard.js +++ b/src/renderStatsCard.js @@ -118,7 +118,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => { Date: Wed, 15 Jul 2020 19:37:04 +0530 Subject: [PATCH 10/10] fix: fixed repo card breaking in absence of primaryLanguage --- src/renderRepoCard.js | 25 +++++++++++-------------- src/renderStatsCard.js | 12 +++++------- src/utils.js | 5 +++++ 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/renderRepoCard.js b/src/renderRepoCard.js index 278e43a..9b127b3 100644 --- a/src/renderRepoCard.js +++ b/src/renderRepoCard.js @@ -1,23 +1,24 @@ -const { kFormatter, encodeHTML, isValidHexColor } = require("../src/utils"); +const { kFormatter, encodeHTML, fallbackColor } = require("../src/utils"); const renderRepoCard = (repo, options = {}) => { const { name, description, primaryLanguage, stargazers, forkCount } = repo; const { title_color, icon_color, text_color, bg_color } = options; + const langName = primaryLanguage ? primaryLanguage.name : "Unspecified"; + const langColor = primaryLanguage ? primaryLanguage.color : "#333"; + const height = 120; - const shiftText = primaryLanguage.name.length > 15 ? 0 : 30; + const shiftText = langName.length > 15 ? 0 : 30; let desc = description || "No description provided"; if (desc.length > 55) { desc = `${description.slice(0, 55)}..`; } - const titleColor = - (isValidHexColor(title_color) && `#${title_color}`) || "#2f80ed"; - const iconColor = - (isValidHexColor(icon_color) && `#${icon_color}`) || "#586069"; - const textColor = (isValidHexColor(text_color) && `#${text_color}`) || "#333"; - const bgColor = (isValidHexColor(bg_color) && `#${bg_color}`) || "#FFFEFE"; + const titleColor = fallbackColor(title_color, "#2f80ed"); + const iconColor = fallbackColor(icon_color, "#586069"); + const textColor = fallbackColor(text_color, "#333"); + const bgColor = fallbackColor(bg_color, "#FFFEFE"); const totalStars = kFormatter(stargazers.totalCount); const totalForks = kFormatter(forkCount); @@ -38,12 +39,8 @@ const renderRepoCard = (repo, options = {}) => { ${encodeHTML(desc)} - - ${ - primaryLanguage.name - } + + ${langName} diff --git a/src/renderStatsCard.js b/src/renderStatsCard.js index a4c6447..1713ac0 100644 --- a/src/renderStatsCard.js +++ b/src/renderStatsCard.js @@ -1,4 +1,4 @@ -const { kFormatter, isValidHexColor } = require("../src/utils"); +const { kFormatter, fallbackColor } = require("../src/utils"); const getStyles = require("./getStyles"); const createTextNode = ({ icon, label, value, id, index, lineHeight }) => { @@ -43,12 +43,10 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => { const lheight = parseInt(line_height); - const titleColor = - (isValidHexColor(title_color) && `#${title_color}`) || "#2f80ed"; - const iconColor = - (isValidHexColor(icon_color) && `#${icon_color}`) || "#4c71f2"; - const textColor = (isValidHexColor(text_color) && `#${text_color}`) || "#333"; - const bgColor = (isValidHexColor(bg_color) && `#${bg_color}`) || "#FFFEFE"; + const titleColor = fallbackColor(title_color, "#2f80ed"); + const iconColor = fallbackColor(icon_color, "#4c71f2"); + const textColor = fallbackColor(text_color, "#333"); + const bgColor = fallbackColor(bg_color, "#FFFEFE"); const STATS = { stars: { diff --git a/src/utils.js b/src/utils.js index 48b2811..09cf660 100644 --- a/src/utils.js +++ b/src/utils.js @@ -43,6 +43,10 @@ function parseBoolean(value) { } } +function fallbackColor(color, fallbackColor) { + return (isValidHexColor(color) && `#${color}`) || fallbackColor; +} + function request(data, headers) { return new Promise((resolve, reject) => { axios({ @@ -65,4 +69,5 @@ module.exports = { isValidHexColor, request, parseBoolean, + fallbackColor, };