diff --git a/api/index.js b/api/index.js index e6684f2..392b694 100644 --- a/api/index.js +++ b/api/index.js @@ -8,6 +8,7 @@ module.exports = async (req, res) => { username, hide, hide_border, + hide_rank, show_icons, line_height, title_color, @@ -29,6 +30,7 @@ module.exports = async (req, res) => { hide: JSON.parse(hide || "[]"), show_icons, hide_border, + hide_rank, line_height, title_color, icon_color, diff --git a/readme.md b/readme.md index 0e02eaf..008ba77 100644 --- a/readme.md +++ b/readme.md @@ -40,6 +40,8 @@ change the `?username=` value to your GitHubs's username [![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)_ + ### Hiding individual stats To hide any specific stats, you can pass a query parameter `?hide=` with an array of items, you wanna hide. @@ -62,6 +64,7 @@ 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_rank=true` hides the ranking ### Customization diff --git a/src/calculateRank.js b/src/calculateRank.js new file mode 100644 index 0000000..9aeeae2 --- /dev/null +++ b/src/calculateRank.js @@ -0,0 +1,52 @@ +function calculateRank({ + totalRepos, + totalCommits, + contributions, + followers, + prs, + issues, + stargazers, +}) { + const COMMITS_OFFSET = 1.65; + const CONTRIBS_OFFSET = 1.65; + const ISSUES_OFFSET = 1; + const STARS_OFFSET = 0.75; + const PRS_OFFSET = 0.5; + const FOLLOWERS_OFFSET = 0.45; + + const FIRST_STEP = 0; + const SECOND_STEP = 5; + const THIRD_STEP = 20; + const FOURTH_STEP = 50; + const FIFTH_STEP = 130; + + // prettier-ignore + const score = ( + totalCommits * COMMITS_OFFSET + + contributions * CONTRIBS_OFFSET + + issues * ISSUES_OFFSET + + stargazers * STARS_OFFSET + + prs * PRS_OFFSET + + followers * FOLLOWERS_OFFSET + ) / totalRepos; + + let level = ""; + + if (score == FIRST_STEP) { + level = "B"; + } else if (score > FIRST_STEP && score <= SECOND_STEP) { + level = "B+"; + } else if (score > SECOND_STEP && score <= THIRD_STEP) { + level = "A"; + } else if (score > THIRD_STEP && score <= FOURTH_STEP) { + level = "A+"; + } else if (score > FOURTH_STEP && score <= FIFTH_STEP) { + level = "A++"; + } else if (score > FIFTH_STEP) { + level = "S+"; + } + + return { level, score }; +} + +module.exports = calculateRank; diff --git a/src/fetchStats.js b/src/fetchStats.js index 8d29170..dc28590 100644 --- a/src/fetchStats.js +++ b/src/fetchStats.js @@ -1,4 +1,5 @@ const { request } = require("./utils"); +const calculateRank = require("./calculateRank"); require("dotenv").config(); async function fetchStats(username) { @@ -22,7 +23,11 @@ async function fetchStats(username) { issues(first: 100) { totalCount } + followers { + totalCount + } repositories(first: 100, orderBy: { direction: DESC, field: STARGAZERS }) { + totalCount nodes { stargazers { totalCount @@ -42,6 +47,7 @@ async function fetchStats(username) { totalIssues: 0, totalStars: 0, contributedTo: 0, + rank: "C", }; if (res.data.errors) { @@ -61,6 +67,16 @@ async function fetchStats(username) { return prev + curr.stargazers.totalCount; }, 0); + stats.rank = calculateRank({ + totalCommits: stats.totalCommits, + totalRepos: user.repositories.totalCount, + followers: user.followers.totalCount, + contributions: stats.contributedTo, + stargazers: stats.totalStars, + prs: stats.totalPRs, + issues: stats.totalIssues, + }); + return stats; } diff --git a/src/renderStatsCard.js b/src/renderStatsCard.js index 56a4c24..8be256b 100644 --- a/src/renderStatsCard.js +++ b/src/renderStatsCard.js @@ -18,11 +18,13 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => { totalIssues, totalPRs, contributedTo, + rank, } = stats; const { hide = [], show_icons = false, hide_border = false, + hide_rank = false, line_height = 25, title_color, icon_color, @@ -81,23 +83,74 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => { .filter((key) => !hide.includes(key)) .map((key) => STAT_MAP[key]); - const height = 45 + (statItems.length + 1) * lheight; + // Calculate the card height depending on how many items there are + // but if rank circle is visible clamp the minimum height to `150` + const height = Math.max( + 45 + (statItems.length + 1) * lheight, + hide_rank ? 0 : 150 + ); + + const border = ` + + `; + + const rankProgress = 180 + rank.score * 0.8; + const rankCircle = hide_rank + ? "" + : ` + + + ${rank.level} + + `; - const border = ``; return ` ${hide_border ? "" : border} - + + ${rankCircle} + ${name}'s GitHub Stats ${statItems.toString().replace(/\,/gm, "")} diff --git a/tests/api.test.js b/tests/api.test.js index 749bc7b..d6c4b4b 100644 --- a/tests/api.test.js +++ b/tests/api.test.js @@ -4,6 +4,7 @@ const MockAdapter = require("axios-mock-adapter"); const api = require("../api/index"); const renderStatsCard = require("../src/renderStatsCard"); const { renderError } = require("../src/utils"); +const calculateRank = require("../src/calculateRank"); const stats = { name: "Anurag Hazra", @@ -12,7 +13,17 @@ const stats = { totalIssues: 300, totalPRs: 400, contributedTo: 500, + rank: null, }; +stats.rank = calculateRank({ + totalCommits: stats.totalCommits, + totalRepos: 1, + followers: 0, + contributions: stats.contributedTo, + stargazers: stats.totalStars, + prs: stats.totalPRs, + issues: stats.totalIssues, +}); const data = { data: { @@ -22,7 +33,9 @@ const data = { contributionsCollection: { totalCommitContributions: stats.totalCommits }, pullRequests: { totalCount: stats.totalPRs }, issues: { totalCount: stats.totalIssues }, + followers: { totalCount: 0 }, repositories: { + totalCount: 1, nodes: [{ stargazers: { totalCount: 100 } }], }, }, diff --git a/tests/calculateRank.test.js b/tests/calculateRank.test.js new file mode 100644 index 0000000..861f66b --- /dev/null +++ b/tests/calculateRank.test.js @@ -0,0 +1,18 @@ +require("@testing-library/jest-dom"); +const calculateRank = require("../src/calculateRank"); + +describe("Test calculateRank", () => { + it("should calculate rank correctly", () => { + expect( + calculateRank({ + totalCommits: 100, + totalRepos: 5, + followers: 100, + contributions: 61, + stargazers: 400, + prs: 300, + issues: 200, + }) + ).toStrictEqual({ level: "S+", score: 192.13 }); + }); +}); diff --git a/tests/fetchStats.test.js b/tests/fetchStats.test.js index 8b8c0c0..8ae45fa 100644 --- a/tests/fetchStats.test.js +++ b/tests/fetchStats.test.js @@ -2,6 +2,7 @@ require("@testing-library/jest-dom"); const axios = require("axios"); const MockAdapter = require("axios-mock-adapter"); const fetchStats = require("../src/fetchStats"); +const calculateRank = require("../src/calculateRank"); const data = { data: { @@ -11,7 +12,9 @@ const data = { contributionsCollection: { totalCommitContributions: 100 }, pullRequests: { totalCount: 300 }, issues: { totalCount: 200 }, + followers: { totalCount: 100 }, repositories: { + totalCount: 5, nodes: [ { stargazers: { totalCount: 100 } }, { stargazers: { totalCount: 100 } }, @@ -46,6 +49,16 @@ describe("Test fetchStats", () => { mock.onPost("https://api.github.com/graphql").reply(200, data); let stats = await fetchStats("anuraghazra"); + const rank = calculateRank({ + totalCommits: 100, + totalRepos: 5, + followers: 100, + contributions: 61, + stargazers: 400, + prs: 300, + issues: 200, + }); + expect(stats).toStrictEqual({ contributedTo: 61, name: "Anurag Hazra", @@ -53,6 +66,7 @@ describe("Test fetchStats", () => { totalIssues: 200, totalPRs: 300, totalStars: 400, + rank, }); }); @@ -63,4 +77,4 @@ describe("Test fetchStats", () => { "Could not fetch user" ); }); -}); \ No newline at end of file +}); diff --git a/tests/renderStatsCard.test.js b/tests/renderStatsCard.test.js index 9536c7f..39ee63c 100644 --- a/tests/renderStatsCard.test.js +++ b/tests/renderStatsCard.test.js @@ -12,6 +12,7 @@ describe("Test renderStatsCard", () => { totalIssues: 300, totalPRs: 400, contributedTo: 500, + rank: { level: "A+", score: 100 }, }; it("should render correctly", () => { @@ -30,6 +31,7 @@ describe("Test renderStatsCard", () => { expect(getByTestId(document.body, "prs").textContent).toBe("400"); expect(getByTestId(document.body, "contribs").textContent).toBe("500"); expect(queryByTestId(document.body, "card-border")).toBeInTheDocument(); + expect(queryByTestId(document.body, "rank-circle")).toBeInTheDocument(); }); it("should hide individual stats", () => { @@ -39,7 +41,8 @@ describe("Test renderStatsCard", () => { expect( document.body.getElementsByTagName("svg")[0].getAttribute("height") - ).toBe("120"); + ).toBe("150"); // height should be 150 because we clamped it. + expect(queryByTestId(document.body, "stars")).toBeDefined(); expect(queryByTestId(document.body, "commits")).toBeDefined(); expect(queryByTestId(document.body, "issues")).toBeNull(); @@ -53,6 +56,12 @@ describe("Test renderStatsCard", () => { expect(queryByTestId(document.body, "card-border")).not.toBeInTheDocument(); }); + it("should hide_rank", () => { + document.body.innerHTML = renderStatsCard(stats, { hide_rank: true }); + + expect(queryByTestId(document.body, "rank-circle")).not.toBeInTheDocument(); + }); + it("should render default colors properly", () => { document.body.innerHTML = renderStatsCard(stats);