Compare commits

...
Author SHA1 Message Date
anuraghazra 6059cb1691 feat: added animations!
Test / build (push) Canceled after 0s
2020-07-14 20:04:36 +05:30
Anurag Hazra cbd0e583ff Merge pull request #48 from anuraghazra/fix-circl-rank-rim
design: added a rim to circle rank's progress
2020-07-14 13:12:56 +05:30
anuraghazra 6f79b495a6 design: added a rim to circle rank's progress 2020-07-14 13:02:45 +05:30
Anurag Hazra 014f710e12 fix: firefox layout breaking (#45)
* fix: firefox layout breaking

* refactor: refactored code and added comments

* chore: remove useragent dep
2020-07-13 23:47:04 +05:30
Anurag Hazra 29d3ea63f8 Merge pull request #40 from anuraghazra/ranking
feat: added rankings
2020-07-13 20:04:52 +05:30
anuraghazra c272a21342 docs: added rank docs
Test / build (push) Canceled after 0s
2020-07-13 19:50:43 +05:30
anuraghazra db49ca7b71 feat: added rankings 2020-07-13 19:41:47 +05:30
Anurag Hazra 8e8d7dd64d Merge pull request #37 from anuraghazra/fix-name-null
fix: fallback to login if name not found
2020-07-13 16:15:50 +05:30
anuraghazra d56ef0d091 fix: fallback to login if name not found
Test / build (push) Canceled after 0s
2020-07-13 16:14:13 +05:30
Anurag Hazra fdeaad36f1 Merge pull request #35 from anuraghazra/readme-update
docs: updated readme
2020-07-13 15:01:40 +05:30
anuraghazra e83c21d9f2 docs: updated readme
Test / build (push) Canceled after 0s
2020-07-13 14:56:59 +05:30
Anurag Hazra 61737f6985 Merge pull request #32 from anuraghazra/fix-stars
fix: fixed total stars count stat
2020-07-12 21:54:17 +05:30
anuraghazra 8e0fa7fbec fix: fixed total stars count stat
Test / build (push) Canceled after 0s
2020-07-12 21:51:56 +05:30
Anurag Hazra 46369f5667 Merge pull request #31 from anuraghazra/integration-tests
tests: added integration tests for the api endpoints
2020-07-12 20:11:44 +05:30
10 changed files with 348 additions and 46 deletions
+2
View File
@@ -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,
+25 -5
View File
@@ -1,10 +1,27 @@
## Github Readme stats
<p align="center">
<h2 align="center">Github Readme Stats</h2>
<p align="center">Get dynamically generated GitHub stats on your readmes!</p>
[![Test](https://github.com/anuraghazra/github-readme-stats/workflows/Test/badge.svg)](https://github.com/anuraghazra/github-readme-stats/actions)
[![GitHub issues](https://img.shields.io/github/issues/anuraghazra/github-readme-stats?color=0088ff)](https://github.com/anuraghazra/github-readme-stats/issues)
[![GitHub pull requests](https://img.shields.io/github/issues-pr/anuraghazra/github-readme-stats?color=0088ff)](https://github.com/anuraghazra/github-readme-stats/pulls)
<p align="center">
<a href="https://github.com/anuraghazra/github-readme-stats/actions">
<img alt="Tests Passing" src="https://github.com/anuraghazra/github-readme-stats/workflows/Test/badge.svg" />
</a>
<a href="https://github.com/anuraghazra/github-readme-stats/issues">
<img alt="Issues" src="https://img.shields.io/github/issues/anuraghazra/github-readme-stats?color=0088ff" />
</a>
<a href="https://github.com/anuraghazra/github-readme-stats/pulls">
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/anuraghazra/github-readme-stats?color=0088ff" />
</a>
</p>
Get dynamically generated GitHub stats on your readmes!
<p align="center">
<a href="#demo">View Demo</a>
·
<a href="https://github.com/anuraghazra/github-readme-stats/issues">Report Bug</a>
·
<a href="https://github.com/anuraghazra/github-readme-stats/issues">Request Feature</a>
</p>
</p>
# Features
@@ -23,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.
@@ -45,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
+52
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;
+20 -3
View File
@@ -1,4 +1,5 @@
const { request } = require("./utils");
const calculateRank = require("./calculateRank");
require("dotenv").config();
async function fetchStats(username) {
@@ -9,6 +10,7 @@ async function fetchStats(username) {
query userInfo($login: String!) {
user(login: $login) {
name
login
repositoriesContributedTo(first: 100, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
totalCount
}
@@ -21,7 +23,11 @@ async function fetchStats(username) {
issues(first: 100) {
totalCount
}
repositories(first: 100) {
followers {
totalCount
}
repositories(first: 100, orderBy: { direction: DESC, field: STARGAZERS }) {
totalCount
nodes {
stargazers {
totalCount
@@ -31,7 +37,7 @@ async function fetchStats(username) {
}
}
`,
variables: { login: username }
variables: { login: username },
});
const stats = {
@@ -41,6 +47,7 @@ async function fetchStats(username) {
totalIssues: 0,
totalStars: 0,
contributedTo: 0,
rank: "C",
};
if (res.data.errors) {
@@ -50,7 +57,7 @@ async function fetchStats(username) {
const user = res.data.data.user;
stats.name = user.name;
stats.name = user.name || user.login;
stats.totalIssues = user.issues.totalCount;
stats.totalCommits = user.contributionsCollection.totalCommitContributions;
stats.totalPRs = user.pullRequests.totalCount;
@@ -60,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;
}
+97
View File
@@ -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;
+94 -35
View File
@@ -1,12 +1,21 @@
const { kFormatter, isValidHexColor } = require("../src/utils");
const getStyles = require("./getStyles");
const createTextNode = ({ icon, label, value, lineHeight, id }) => {
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 <tspan dy="" />
// to fix firefox layout bug
const lheight = lineHeight * (index + 1);
return `
<tspan x="25" dy="${lineHeight}" class="stat bold">
<tspan data-testid="icon" class="icon ${classname}">${icon}</tspan> ${label}:</tspan>
<tspan data-testid="${id}" x="155" dy="0" class="stat">${kValue}</tspan>
<text class="stagger" style="animation-delay: ${staggerDelay}ms" x="25" y="${lheight}">
<tspan dx="0" data-testid="icon" class="icon ${classname}">${icon}</tspan>
<tspan dx="0" class="stat bold">
${label}:
</tspan>
<tspan x="160" data-testid="${id}" class="stat">${kValue}</tspan>
</text>
`;
};
@@ -18,11 +27,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,
@@ -39,69 +50,117 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
const textColor = (isValidHexColor(text_color) && `#${text_color}`) || "#333";
const bgColor = (isValidHexColor(bg_color) && `#${bg_color}`) || "#FFFEFE";
const STAT_MAP = {
stars: createTextNode({
const STATS = {
stars: {
icon: "★",
label: "Total Stars",
value: totalStars,
lineHeight: lheight,
id: "stars",
}),
commits: createTextNode({
},
commits: {
icon: "🕗",
label: "Total Commits",
value: totalCommits,
lineHeight: lheight,
id: "commits",
}),
prs: createTextNode({
},
prs: {
icon: "🔀",
label: "Total PRs",
value: totalPRs,
lineHeight: lheight,
id: "prs",
}),
issues: createTextNode({
},
issues: {
icon: "ⓘ",
label: "Total Issues",
value: totalIssues,
lineHeight: lheight,
id: "issues",
}),
contribs: createTextNode({
},
contribs: {
icon: "📕",
label: "Contributed to",
value: contributedTo,
lineHeight: lheight,
id: "contribs",
}),
},
};
const statItems = Object.keys(STAT_MAP)
const statItems = Object.keys(STATS)
.filter((key) => !hide.includes(key))
.map((key) => STAT_MAP[key]);
.map((key, index) =>
// create the text nodes, and pass index so that we can calculate the line spacing
createTextNode({ ...STATS[key], index, lineHeight: lheight })
);
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 rankCircle = hide_rank
? ""
: `<g data-testid="rank-circle" transform="translate(400, ${
height / 1.85
})">
<circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
<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"
>
${rank.level}
</text>
</g>`;
// 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,
});
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 `
<svg width="495" height="${height}" viewBox="0 0 495 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
.header { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor}; }
.stat { font: 600 14px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor}; }
.star-icon { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; }
.bold { font-weight: 700 }
.icon {
fill: ${iconColor};
display: ${!!show_icons ? "block" : "none"};
}
${styles}
</style>
${hide_border ? "" : border}
${rankCircle}
<text x="25" y="35" class="header">${name}'s GitHub Stats</text>
<text y="45">
<svg x="0" y="45">
${statItems.toString().replace(/\,/gm, "")}
</text>
</svg>
</svg>
`;
};
+13
View File
@@ -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 } }],
},
},
+18
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 });
});
});
+15 -1
View File
@@ -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"
);
});
});
});
+12 -2
View File
@@ -12,6 +12,7 @@ describe("Test renderStatsCard", () => {
totalIssues: 300,
totalPRs: 400,
contributedTo: 500,
rank: { level: "A+", score: 40 },
};
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,11 +56,18 @@ 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);
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"];