mirror of
https://github.com/Aviortheking/codestats-readme.git
synced 2025-04-22 10:42:08 +00:00
feat: added rankings
This commit is contained in:
parent
8e8d7dd64d
commit
db49ca7b71
@ -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
52
src/calculateRank.js
Normal 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;
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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, "")}
|
||||||
|
@ -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 } }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
18
tests/calculateRank.test.js
Normal file
18
tests/calculateRank.test.js
Normal 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 });
|
||||||
|
});
|
||||||
|
});
|
@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user