feat: added rankings

This commit is contained in:
anuraghazra 2020-07-13 19:41:47 +05:30
parent 8e8d7dd64d
commit db49ca7b71
8 changed files with 182 additions and 5 deletions

View File

@ -8,6 +8,7 @@ module.exports = async (req, res) => {
username, username,
hide, hide,
hide_border, hide_border,
hide_rank,
show_icons, show_icons,
line_height, line_height,
title_color, title_color,
@ -29,6 +30,7 @@ module.exports = async (req, res) => {
hide: JSON.parse(hide || "[]"), hide: JSON.parse(hide || "[]"),
show_icons, show_icons,
hide_border, hide_border,
hide_rank,
line_height, line_height,
title_color, title_color,
icon_color, icon_color,

52
src/calculateRank.js Normal file
View File

@ -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;

View File

@ -1,4 +1,5 @@
const { request } = require("./utils"); const { request } = require("./utils");
const calculateRank = require("./calculateRank");
require("dotenv").config(); require("dotenv").config();
async function fetchStats(username) { async function fetchStats(username) {
@ -22,7 +23,11 @@ async function fetchStats(username) {
issues(first: 100) { issues(first: 100) {
totalCount totalCount
} }
followers {
totalCount
}
repositories(first: 100, orderBy: { direction: DESC, field: STARGAZERS }) { repositories(first: 100, orderBy: { direction: DESC, field: STARGAZERS }) {
totalCount
nodes { nodes {
stargazers { stargazers {
totalCount totalCount
@ -42,6 +47,7 @@ async function fetchStats(username) {
totalIssues: 0, totalIssues: 0,
totalStars: 0, totalStars: 0,
contributedTo: 0, contributedTo: 0,
rank: "C",
}; };
if (res.data.errors) { if (res.data.errors) {
@ -61,6 +67,16 @@ async function fetchStats(username) {
return prev + curr.stargazers.totalCount; return prev + curr.stargazers.totalCount;
}, 0); }, 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; return stats;
} }

View File

@ -18,11 +18,13 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
totalIssues, totalIssues,
totalPRs, totalPRs,
contributedTo, contributedTo,
rank,
} = stats; } = stats;
const { const {
hide = [], hide = [],
show_icons = false, show_icons = false,
hide_border = false, hide_border = false,
hide_rank = false,
line_height = 25, line_height = 25,
title_color, title_color,
icon_color, icon_color,
@ -81,23 +83,74 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
.filter((key) => !hide.includes(key)) .filter((key) => !hide.includes(key))
.map((key) => STAT_MAP[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 = `
<rect
data-testid="card-border"
x="0.5"
y="0.5"
width="494"
height="99%"
rx="4.5"
fill="${bgColor}"
stroke="#E4E2E2"
/>
`;
const rankProgress = 180 + rank.score * 0.8;
const rankCircle = hide_rank
? ""
: `<g data-testid="rank-circle" transform="translate(400, ${
height / 1.85
})">
<circle class="rank-circle" cx="-10" cy="8" r="40" />
<text
x="0"
y="0"
alignment-baseline="central"
dominant-baseline="central"
text-anchor="middle"
class="rank-text"
transform="translate(-5, 5)"
>
${rank.level}
</text>
</g>`;
const border = `<rect data-testid="card-border" x="0.5" y="0.5" width="494" height="99%" rx="4.5" fill="${bgColor}" stroke="#E4E2E2"/>`;
return ` return `
<svg width="495" height="${height}" viewBox="0 0 495 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="495" height="${height}" viewBox="0 0 495 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
<style> <style>
.header { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor}; } .header { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor}; }
.stat { font: 600 14px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor}; } .stat { font: 600 14px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor}; }
.rank-text { font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor}; }
.star-icon { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; } .star-icon { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; }
.bold { font-weight: 700 } .bold { font-weight: 700 }
.icon { .icon {
fill: ${iconColor}; fill: ${iconColor};
display: ${!!show_icons ? "block" : "none"}; display: ${!!show_icons ? "block" : "none"};
} }
.rank-circle {
stroke-dashoffset: 30;
stroke-dasharray: ${rankProgress};
stroke: ${titleColor};
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
}
</style> </style>
${hide_border ? "" : border} ${hide_border ? "" : border}
${rankCircle}
<text x="25" y="35" class="header">${name}'s GitHub Stats</text> <text x="25" y="35" class="header">${name}'s GitHub Stats</text>
<text y="45"> <text y="45">
${statItems.toString().replace(/\,/gm, "")} ${statItems.toString().replace(/\,/gm, "")}

View File

@ -4,6 +4,7 @@ const MockAdapter = require("axios-mock-adapter");
const api = require("../api/index"); const api = require("../api/index");
const renderStatsCard = require("../src/renderStatsCard"); const renderStatsCard = require("../src/renderStatsCard");
const { renderError } = require("../src/utils"); const { renderError } = require("../src/utils");
const calculateRank = require("../src/calculateRank");
const stats = { const stats = {
name: "Anurag Hazra", name: "Anurag Hazra",
@ -12,7 +13,17 @@ const stats = {
totalIssues: 300, totalIssues: 300,
totalPRs: 400, totalPRs: 400,
contributedTo: 500, 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 = { const data = {
data: { data: {
@ -22,7 +33,9 @@ const data = {
contributionsCollection: { totalCommitContributions: stats.totalCommits }, contributionsCollection: { totalCommitContributions: stats.totalCommits },
pullRequests: { totalCount: stats.totalPRs }, pullRequests: { totalCount: stats.totalPRs },
issues: { totalCount: stats.totalIssues }, issues: { totalCount: stats.totalIssues },
followers: { totalCount: 0 },
repositories: { repositories: {
totalCount: 1,
nodes: [{ stargazers: { totalCount: 100 } }], nodes: [{ stargazers: { totalCount: 100 } }],
}, },
}, },

View File

@ -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 });
});
});

View File

@ -2,6 +2,7 @@ require("@testing-library/jest-dom");
const axios = require("axios"); const axios = require("axios");
const MockAdapter = require("axios-mock-adapter"); const MockAdapter = require("axios-mock-adapter");
const fetchStats = require("../src/fetchStats"); const fetchStats = require("../src/fetchStats");
const calculateRank = require("../src/calculateRank");
const data = { const data = {
data: { data: {
@ -11,7 +12,9 @@ const data = {
contributionsCollection: { totalCommitContributions: 100 }, contributionsCollection: { totalCommitContributions: 100 },
pullRequests: { totalCount: 300 }, pullRequests: { totalCount: 300 },
issues: { totalCount: 200 }, issues: { totalCount: 200 },
followers: { totalCount: 100 },
repositories: { repositories: {
totalCount: 5,
nodes: [ nodes: [
{ stargazers: { totalCount: 100 } }, { stargazers: { totalCount: 100 } },
{ stargazers: { totalCount: 100 } }, { stargazers: { totalCount: 100 } },
@ -46,6 +49,16 @@ describe("Test fetchStats", () => {
mock.onPost("https://api.github.com/graphql").reply(200, data); mock.onPost("https://api.github.com/graphql").reply(200, data);
let stats = await fetchStats("anuraghazra"); 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({ expect(stats).toStrictEqual({
contributedTo: 61, contributedTo: 61,
name: "Anurag Hazra", name: "Anurag Hazra",
@ -53,6 +66,7 @@ describe("Test fetchStats", () => {
totalIssues: 200, totalIssues: 200,
totalPRs: 300, totalPRs: 300,
totalStars: 400, totalStars: 400,
rank,
}); });
}); });

View File

@ -12,6 +12,7 @@ describe("Test renderStatsCard", () => {
totalIssues: 300, totalIssues: 300,
totalPRs: 400, totalPRs: 400,
contributedTo: 500, contributedTo: 500,
rank: { level: "A+", score: 100 },
}; };
it("should render correctly", () => { it("should render correctly", () => {
@ -30,6 +31,7 @@ describe("Test renderStatsCard", () => {
expect(getByTestId(document.body, "prs").textContent).toBe("400"); expect(getByTestId(document.body, "prs").textContent).toBe("400");
expect(getByTestId(document.body, "contribs").textContent).toBe("500"); expect(getByTestId(document.body, "contribs").textContent).toBe("500");
expect(queryByTestId(document.body, "card-border")).toBeInTheDocument(); expect(queryByTestId(document.body, "card-border")).toBeInTheDocument();
expect(queryByTestId(document.body, "rank-circle")).toBeInTheDocument();
}); });
it("should hide individual stats", () => { it("should hide individual stats", () => {
@ -39,7 +41,8 @@ describe("Test renderStatsCard", () => {
expect( expect(
document.body.getElementsByTagName("svg")[0].getAttribute("height") 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, "stars")).toBeDefined();
expect(queryByTestId(document.body, "commits")).toBeDefined(); expect(queryByTestId(document.body, "commits")).toBeDefined();
expect(queryByTestId(document.body, "issues")).toBeNull(); expect(queryByTestId(document.body, "issues")).toBeNull();
@ -53,6 +56,12 @@ describe("Test renderStatsCard", () => {
expect(queryByTestId(document.body, "card-border")).not.toBeInTheDocument(); 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", () => { it("should render default colors properly", () => {
document.body.innerHTML = renderStatsCard(stats); document.body.innerHTML = renderStatsCard(stats);