Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c272a21342 | ||
|
|
db49ca7b71 | ||
|
|
8e8d7dd64d | ||
|
|
d56ef0d091 | ||
|
|
fdeaad36f1 | ||
|
|
e83c21d9f2 | ||
|
|
61737f6985 | ||
|
|
8e0fa7fbec | ||
|
|
46369f5667 | ||
|
|
40e0937100 | ||
|
|
3e9ce48777 | ||
|
|
5f4ada0afb | ||
|
|
4b3e866478 | ||
|
|
b2aa92c53e | ||
|
|
9a87dc02a2 | ||
|
|
7a7ad717a8 | ||
|
|
47eb35a7ec | ||
|
|
5cf82b0194 | ||
|
|
208076e7f4 | ||
|
|
9af56df2c1 | ||
|
|
61a0f517db | ||
|
|
001062db21 | ||
|
|
59b8bded9c | ||
|
|
c25319ee6c | ||
|
|
73d26e78fd | ||
|
|
2d691da3aa | ||
|
|
61a2866aff | ||
|
|
7e3278c708 |
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Anurag Hazra
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+17
-1
@@ -4,7 +4,18 @@ const fetchStats = require("../src/fetchStats");
|
||||
const renderStatsCard = require("../src/renderStatsCard");
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
const { username, hide, hide_border, show_icons, line_height } = req.query;
|
||||
const {
|
||||
username,
|
||||
hide,
|
||||
hide_border,
|
||||
hide_rank,
|
||||
show_icons,
|
||||
line_height,
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
} = req.query;
|
||||
let stats;
|
||||
|
||||
res.setHeader("Content-Type", "image/svg+xml");
|
||||
@@ -19,7 +30,12 @@ module.exports = async (req, res) => {
|
||||
hide: JSON.parse(hide || "[]"),
|
||||
show_icons,
|
||||
hide_border,
|
||||
hide_rank,
|
||||
line_height,
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
+16
-2
@@ -4,7 +4,14 @@ const fetchRepo = require("../src/fetchRepo");
|
||||
const renderRepoCard = require("../src/renderRepoCard");
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
const { username, repo } = req.query;
|
||||
const {
|
||||
username,
|
||||
repo,
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
} = req.query;
|
||||
|
||||
let repoData;
|
||||
res.setHeader("Content-Type", "image/svg+xml");
|
||||
@@ -16,5 +23,12 @@ module.exports = async (req, res) => {
|
||||
return res.send(renderError(err.message));
|
||||
}
|
||||
|
||||
res.send(renderRepoCard(repoData));
|
||||
res.send(
|
||||
renderRepoCard(repoData, {
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
Generated
-5054
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@
|
||||
"@testing-library/jest-dom": "^5.11.0",
|
||||
"axios": "^0.19.2",
|
||||
"axios-mock-adapter": "^1.18.1",
|
||||
"css-to-object": "^1.1.0",
|
||||
"jest": "^26.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,11 +1,33 @@
|
||||
## 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>
|
||||
|
||||
Get dynamically generated GitHub stats on your readmes!
|
||||
<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>
|
||||
|
||||
<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
|
||||
|
||||
- [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
|
||||
@@ -18,6 +40,8 @@ change the `?username=` value to your GitHubs's username
|
||||
[](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.
|
||||
@@ -40,6 +64,26 @@ 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
|
||||
|
||||
You can customize the appearance of your `Stats Card` or `Repo Card` however you want with url params.
|
||||
|
||||
Customization Options:
|
||||
|
||||
| Option | type | Stats Card (default) | Repo Card (default) |
|
||||
| ----------- | --------- | ---------------------- | ---------------------- |
|
||||
| title_color | hex color | #2f80ed | #2f80ed |
|
||||
| text_color | hex color | #333 | #333 |
|
||||
| icon_color | hex color | #4c71f2 | #586069 |
|
||||
| bg_color | hex color | rgba(255, 255, 255, 0) | rgba(255, 255, 255, 0) |
|
||||
|
||||
- You can also customize the cards to be compatible with dark mode
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### Demo
|
||||
|
||||
@@ -55,6 +99,14 @@ Other options:
|
||||
|
||||

|
||||
|
||||
- Customizing stats card
|
||||
|
||||

|
||||
|
||||
- Customizing repo card
|
||||
|
||||

|
||||
|
||||
# Github Extra Pins
|
||||
|
||||
Github extra pins allow you to pin more than 6 repositories in your profile using a GitHub readme profile.
|
||||
|
||||
@@ -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;
|
||||
+28
-35
@@ -1,48 +1,41 @@
|
||||
const axios = require("axios");
|
||||
const { request } = require("./utils");
|
||||
|
||||
async function fetchRepo(username, reponame) {
|
||||
if (!username || !reponame) {
|
||||
throw new Error("Invalid username or reponame");
|
||||
}
|
||||
|
||||
const res = await axios({
|
||||
url: "https://api.github.com/graphql",
|
||||
method: "post",
|
||||
headers: {
|
||||
Authorization: `bearer ${process.env.GITHUB_TOKEN}`,
|
||||
},
|
||||
data: {
|
||||
query: `
|
||||
fragment RepoInfo on Repository {
|
||||
const res = await request({
|
||||
query: `
|
||||
fragment RepoInfo on Repository {
|
||||
name
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
description
|
||||
primaryLanguage {
|
||||
color
|
||||
id
|
||||
name
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
description
|
||||
primaryLanguage {
|
||||
color
|
||||
id
|
||||
name
|
||||
}
|
||||
forkCount
|
||||
}
|
||||
query getRepo($login: String!, $repo: String!) {
|
||||
user(login: $login) {
|
||||
repository(name: $repo) {
|
||||
...RepoInfo
|
||||
}
|
||||
}
|
||||
organization(login: $login) {
|
||||
repository(name: $repo) {
|
||||
...RepoInfo
|
||||
}
|
||||
forkCount
|
||||
}
|
||||
query getRepo($login: String!, $repo: String!) {
|
||||
user(login: $login) {
|
||||
repository(name: $repo) {
|
||||
...RepoInfo
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
login: username,
|
||||
repo: reponame,
|
||||
},
|
||||
organization(login: $login) {
|
||||
repository(name: $repo) {
|
||||
...RepoInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
login: username,
|
||||
repo: reponame,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+43
-35
@@ -1,46 +1,43 @@
|
||||
const axios = require("axios");
|
||||
const { request } = require("./utils");
|
||||
const calculateRank = require("./calculateRank");
|
||||
require("dotenv").config();
|
||||
|
||||
async function fetchStats(username) {
|
||||
if (!username) throw Error("Invalid username");
|
||||
|
||||
const res = await axios({
|
||||
url: "https://api.github.com/graphql",
|
||||
method: "post",
|
||||
headers: {
|
||||
Authorization: `bearer ${process.env.GITHUB_TOKEN}`,
|
||||
},
|
||||
data: {
|
||||
query: `
|
||||
query userInfo($login: String!) {
|
||||
user(login: $login) {
|
||||
name
|
||||
repositoriesContributedTo(first: 100, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
|
||||
totalCount
|
||||
}
|
||||
contributionsCollection {
|
||||
totalCommitContributions
|
||||
}
|
||||
pullRequests(first: 100) {
|
||||
totalCount
|
||||
}
|
||||
issues(first: 100) {
|
||||
totalCount
|
||||
}
|
||||
repositories(first: 100) {
|
||||
nodes {
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
const res = await 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) {
|
||||
totalCount
|
||||
}
|
||||
issues(first: 100) {
|
||||
totalCount
|
||||
}
|
||||
followers {
|
||||
totalCount
|
||||
}
|
||||
repositories(first: 100, orderBy: { direction: DESC, field: STARGAZERS }) {
|
||||
totalCount
|
||||
nodes {
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
login: username,
|
||||
},
|
||||
},
|
||||
}
|
||||
`,
|
||||
variables: { login: username },
|
||||
});
|
||||
|
||||
const stats = {
|
||||
@@ -50,6 +47,7 @@ async function fetchStats(username) {
|
||||
totalIssues: 0,
|
||||
totalStars: 0,
|
||||
contributedTo: 0,
|
||||
rank: "C",
|
||||
};
|
||||
|
||||
if (res.data.errors) {
|
||||
@@ -59,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;
|
||||
@@ -69,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;
|
||||
}
|
||||
|
||||
|
||||
+19
-12
@@ -1,7 +1,9 @@
|
||||
const { kFormatter, encodeHTML } = require("../src/utils");
|
||||
const { kFormatter, encodeHTML, isValidHexColor } = require("../src/utils");
|
||||
|
||||
const renderRepoCard = (repo) => {
|
||||
const renderRepoCard = (repo, options = {}) => {
|
||||
const { name, description, primaryLanguage, stargazers, forkCount } = repo;
|
||||
const { title_color, icon_color, text_color, bg_color } = options;
|
||||
|
||||
const height = 120;
|
||||
const shiftText = primaryLanguage.name.length > 15 ? 0 : 30;
|
||||
|
||||
@@ -10,20 +12,25 @@ const renderRepoCard = (repo) => {
|
||||
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 totalStars = kFormatter(stargazers.totalCount);
|
||||
const totalForks = kFormatter(forkCount);
|
||||
return `
|
||||
<svg width="400" height="${height}" viewBox="0 0 400 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.header { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2F80ED }
|
||||
.stat { font: 600 14px 'Segoe UI', Ubuntu, Sans-Serif; fill: #333 }
|
||||
.star-icon { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; }
|
||||
.bold { font-weight: 700 }
|
||||
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: #586069 }
|
||||
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #586069 }
|
||||
.header { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor} }
|
||||
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
|
||||
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
|
||||
.icon { fill: ${iconColor} }
|
||||
</style>
|
||||
<rect x="0.5" y="0.5" width="399" height="99%" rx="4.5" fill="#FFFEFE" stroke="#E4E2E2"/>
|
||||
<svg x="25" y="25" viewBox="0 0 16 16" version="1.1" width="16" height="16" fill="#586069">
|
||||
<rect data-testid="card-border" x="0.5" y="0.5" width="399" height="99%" rx="4.5" fill="${bgColor}" stroke="#E4E2E2"/>
|
||||
<svg class="icon" x="25" y="25" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"></path>
|
||||
</svg>
|
||||
|
||||
@@ -40,14 +47,14 @@ const renderRepoCard = (repo) => {
|
||||
</g>
|
||||
|
||||
<g transform="translate(${155 - shiftText}, 100)">
|
||||
<svg y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16" fill="#586069">
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path>
|
||||
</svg>
|
||||
<text data-testid="stargazers" class="gray" x="25">${totalStars}</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(${220 - shiftText}, 100)">
|
||||
<svg y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16" fill="#586069">
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path>
|
||||
</svg>
|
||||
<text data-testid="forkcount" class="gray" x="25">${totalForks}</text>
|
||||
|
||||
+72
-7
@@ -1,11 +1,11 @@
|
||||
const { kFormatter } = require("../src/utils");
|
||||
const { kFormatter, isValidHexColor } = require("../src/utils");
|
||||
|
||||
const createTextNode = ({ icon, label, value, lineHeight, id }) => {
|
||||
const classname = icon === "★" && "star-icon";
|
||||
const kValue = kFormatter(value);
|
||||
return `
|
||||
<tspan x="25" dy="${lineHeight}" class="stat bold">
|
||||
<tspan data-testid="icon" class="icon ${classname}" fill="#4C71F2">${icon}</tspan> ${label}:</tspan>
|
||||
<tspan data-testid="icon" class="icon ${classname}">${icon}</tspan> ${label}:</tspan>
|
||||
<tspan data-testid="${id}" x="155" dy="0" class="stat">${kValue}</tspan>
|
||||
`;
|
||||
};
|
||||
@@ -18,16 +18,29 @@ 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,
|
||||
text_color,
|
||||
bg_color,
|
||||
} = options;
|
||||
|
||||
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 STAT_MAP = {
|
||||
stars: createTextNode({
|
||||
icon: "★",
|
||||
@@ -70,22 +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 = `
|
||||
<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="#FFFEFE" 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: #2F80ED }
|
||||
.stat { font: 600 14px 'Segoe UI', Ubuntu, Sans-Serif; fill: #333 }
|
||||
.header { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor}; }
|
||||
.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; }
|
||||
.bold { font-weight: 700 }
|
||||
.icon {
|
||||
fill: ${iconColor};
|
||||
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>
|
||||
${hide_border ? "" : border}
|
||||
|
||||
|
||||
${rankCircle}
|
||||
|
||||
<text x="25" y="35" class="header">${name}'s GitHub Stats</text>
|
||||
<text y="45">
|
||||
${statItems.toString().replace(/\,/gm, "")}
|
||||
|
||||
+24
-1
@@ -1,3 +1,5 @@
|
||||
const axios = require("axios");
|
||||
|
||||
const renderError = (message) => {
|
||||
return `
|
||||
<svg width="495" height="100" viewBox="0 0 495 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -25,4 +27,25 @@ function kFormatter(num) {
|
||||
: Math.sign(num) * Math.abs(num);
|
||||
}
|
||||
|
||||
module.exports = { renderError, kFormatter, encodeHTML };
|
||||
function isValidHexColor(hexColor) {
|
||||
return new RegExp(
|
||||
/^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4})$/
|
||||
).test(hexColor);
|
||||
}
|
||||
|
||||
function request(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios({
|
||||
url: "https://api.github.com/graphql",
|
||||
method: "post",
|
||||
headers: {
|
||||
Authorization: `bearer ${process.env.GITHUB_TOKEN}`,
|
||||
},
|
||||
data,
|
||||
})
|
||||
.then((response) => resolve(response))
|
||||
.catch((error) => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { renderError, kFormatter, encodeHTML, isValidHexColor, request };
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
require("@testing-library/jest-dom");
|
||||
const axios = require("axios");
|
||||
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",
|
||||
totalStars: 100,
|
||||
totalCommits: 200,
|
||||
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: {
|
||||
user: {
|
||||
name: stats.name,
|
||||
repositoriesContributedTo: { totalCount: stats.contributedTo },
|
||||
contributionsCollection: { totalCommitContributions: stats.totalCommits },
|
||||
pullRequests: { totalCount: stats.totalPRs },
|
||||
issues: { totalCount: stats.totalIssues },
|
||||
followers: { totalCount: 0 },
|
||||
repositories: {
|
||||
totalCount: 1,
|
||||
nodes: [{ stargazers: { totalCount: 100 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const error = {
|
||||
errors: [
|
||||
{
|
||||
type: "NOT_FOUND",
|
||||
path: ["user"],
|
||||
locations: [],
|
||||
message: "Could not fetch user",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mock = new MockAdapter(axios);
|
||||
|
||||
afterEach(() => {
|
||||
mock.reset();
|
||||
});
|
||||
|
||||
describe("Test /api/", () => {
|
||||
it("should test the request", async () => {
|
||||
const req = {
|
||||
query: {
|
||||
username: "anuraghazra",
|
||||
},
|
||||
};
|
||||
const res = {
|
||||
setHeader: jest.fn(),
|
||||
send: jest.fn(),
|
||||
};
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, data);
|
||||
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
|
||||
expect(res.send).toBeCalledWith(renderStatsCard(stats, { ...req.query }));
|
||||
});
|
||||
|
||||
it("should render error card on error", async () => {
|
||||
const req = {
|
||||
query: {
|
||||
username: "anuraghazra",
|
||||
},
|
||||
};
|
||||
const res = {
|
||||
setHeader: jest.fn(),
|
||||
send: jest.fn(),
|
||||
};
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, error);
|
||||
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
|
||||
expect(res.send).toBeCalledWith(renderError(error.errors[0].message));
|
||||
});
|
||||
|
||||
it("should get the query options", async () => {
|
||||
const req = {
|
||||
query: {
|
||||
username: "anuraghazra",
|
||||
hide: `["issues","prs","contribs"]`,
|
||||
show_icons: true,
|
||||
hide_border: true,
|
||||
line_height: 100,
|
||||
title_color: "fff",
|
||||
icon_color: "fff",
|
||||
text_color: "fff",
|
||||
bg_color: "fff",
|
||||
},
|
||||
};
|
||||
const res = {
|
||||
setHeader: jest.fn(),
|
||||
send: jest.fn(),
|
||||
};
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, data);
|
||||
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
|
||||
expect(res.send).toBeCalledWith(
|
||||
renderStatsCard(stats, {
|
||||
hide: ["issues", "prs", "contribs"],
|
||||
show_icons: true,
|
||||
hide_border: true,
|
||||
line_height: 100,
|
||||
title_color: "fff",
|
||||
icon_color: "fff",
|
||||
text_color: "fff",
|
||||
bg_color: "fff",
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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 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,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
require("@testing-library/jest-dom");
|
||||
const axios = require("axios");
|
||||
const MockAdapter = require("axios-mock-adapter");
|
||||
const pin = require("../api/pin");
|
||||
const renderRepoCard = require("../src/renderRepoCard");
|
||||
const { renderError } = require("../src/utils");
|
||||
|
||||
const data_repo = {
|
||||
repository: {
|
||||
name: "convoychat",
|
||||
stargazers: { totalCount: 38000 },
|
||||
description: "Help us take over the world! React + TS + GraphQL Chat App",
|
||||
primaryLanguage: {
|
||||
color: "#2b7489",
|
||||
id: "MDg6TGFuZ3VhZ2UyODc=",
|
||||
name: "TypeScript",
|
||||
},
|
||||
forkCount: 100,
|
||||
},
|
||||
};
|
||||
|
||||
const data_user = {
|
||||
data: {
|
||||
user: { repository: data_repo.repository },
|
||||
organization: null,
|
||||
},
|
||||
};
|
||||
|
||||
const mock = new MockAdapter(axios);
|
||||
|
||||
afterEach(() => {
|
||||
mock.reset();
|
||||
});
|
||||
|
||||
describe("Test /api/pin", () => {
|
||||
it("should test the request", async () => {
|
||||
const req = {
|
||||
query: {
|
||||
username: "anuraghazra",
|
||||
repo: "convoychat",
|
||||
},
|
||||
};
|
||||
const res = {
|
||||
setHeader: jest.fn(),
|
||||
send: jest.fn(),
|
||||
};
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
|
||||
|
||||
await pin(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
|
||||
expect(res.send).toBeCalledWith(renderRepoCard(data_repo.repository));
|
||||
});
|
||||
|
||||
it("should get the query options", async () => {
|
||||
const req = {
|
||||
query: {
|
||||
username: "anuraghazra",
|
||||
repo: "convoychat",
|
||||
title_color: "fff",
|
||||
icon_color: "fff",
|
||||
text_color: "fff",
|
||||
bg_color: "fff",
|
||||
},
|
||||
};
|
||||
const res = {
|
||||
setHeader: jest.fn(),
|
||||
send: jest.fn(),
|
||||
};
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
|
||||
|
||||
await pin(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
|
||||
expect(res.send).toBeCalledWith(
|
||||
renderRepoCard(data_repo.repository, { ...req.query })
|
||||
);
|
||||
});
|
||||
|
||||
it("should render error card if user repo not found", async () => {
|
||||
const req = {
|
||||
query: {
|
||||
username: "anuraghazra",
|
||||
repo: "convoychat",
|
||||
},
|
||||
};
|
||||
const res = {
|
||||
setHeader: jest.fn(),
|
||||
send: jest.fn(),
|
||||
};
|
||||
mock
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.reply(200, { data: { user: { repository: null }, organization: null } });
|
||||
|
||||
await pin(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
|
||||
expect(res.send).toBeCalledWith(renderError("User Repository Not found"));
|
||||
});
|
||||
|
||||
it("should render error card if org repo not found", async () => {
|
||||
const req = {
|
||||
query: {
|
||||
username: "anuraghazra",
|
||||
repo: "convoychat",
|
||||
},
|
||||
};
|
||||
const res = {
|
||||
setHeader: jest.fn(),
|
||||
send: jest.fn(),
|
||||
};
|
||||
mock
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.reply(200, { data: { user: null, organization: { repository: null } } });
|
||||
|
||||
await pin(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
|
||||
expect(res.send).toBeCalledWith(
|
||||
renderError("Organization Repository Not found")
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
require("@testing-library/jest-dom");
|
||||
const cssToObject = require("css-to-object");
|
||||
const renderRepoCard = require("../src/renderRepoCard");
|
||||
|
||||
const { queryByTestId } = require("@testing-library/dom");
|
||||
@@ -88,4 +89,51 @@ describe("Test renderRepoCard", () => {
|
||||
"translate(125, 100)"
|
||||
);
|
||||
});
|
||||
|
||||
it("should render default colors properly", () => {
|
||||
document.body.innerHTML = renderRepoCard(data_repo.repository);
|
||||
|
||||
const styleTag = document.querySelector("style");
|
||||
const stylesObject = cssToObject(styleTag.innerHTML);
|
||||
|
||||
const headerClassStyles = stylesObject[".header"];
|
||||
const statClassStyles = stylesObject[".description"];
|
||||
const iconClassStyles = stylesObject[".icon"];
|
||||
|
||||
expect(headerClassStyles.fill).toBe("#2f80ed");
|
||||
expect(statClassStyles.fill).toBe("#333");
|
||||
expect(iconClassStyles.fill).toBe("#586069");
|
||||
expect(queryByTestId(document.body, "card-border")).toHaveAttribute(
|
||||
"fill",
|
||||
"#FFFEFE"
|
||||
);
|
||||
});
|
||||
|
||||
it("should render custom colors properly", () => {
|
||||
const customColors = {
|
||||
title_color: "5a0",
|
||||
icon_color: "1b998b",
|
||||
text_color: "9991",
|
||||
bg_color: "252525",
|
||||
};
|
||||
|
||||
document.body.innerHTML = renderRepoCard(data_repo.repository, {
|
||||
...customColors,
|
||||
});
|
||||
|
||||
const styleTag = document.querySelector("style");
|
||||
const stylesObject = cssToObject(styleTag.innerHTML);
|
||||
|
||||
const headerClassStyles = stylesObject[".header"];
|
||||
const statClassStyles = stylesObject[".description"];
|
||||
const iconClassStyles = stylesObject[".icon"];
|
||||
|
||||
expect(headerClassStyles.fill).toBe(`#${customColors.title_color}`);
|
||||
expect(statClassStyles.fill).toBe(`#${customColors.text_color}`);
|
||||
expect(iconClassStyles.fill).toBe(`#${customColors.icon_color}`);
|
||||
expect(queryByTestId(document.body, "card-border")).toHaveAttribute(
|
||||
"fill",
|
||||
"#252525"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
require("@testing-library/jest-dom");
|
||||
const cssToObject = require("css-to-object");
|
||||
const renderStatsCard = require("../src/renderStatsCard");
|
||||
|
||||
const { getByTestId, queryByTestId } = require("@testing-library/dom");
|
||||
@@ -11,6 +12,7 @@ describe("Test renderStatsCard", () => {
|
||||
totalIssues: 300,
|
||||
totalPRs: 400,
|
||||
contributedTo: 500,
|
||||
rank: { level: "A+", score: 100 },
|
||||
};
|
||||
|
||||
it("should render correctly", () => {
|
||||
@@ -29,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", () => {
|
||||
@@ -38,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();
|
||||
@@ -51,4 +55,55 @@ 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);
|
||||
|
||||
const headerClassStyles = stylesObject[".header"];
|
||||
const statClassStyles = stylesObject[".stat"];
|
||||
const iconClassStyles = stylesObject[".icon"];
|
||||
|
||||
expect(headerClassStyles.fill).toBe("#2f80ed");
|
||||
expect(statClassStyles.fill).toBe("#333");
|
||||
expect(iconClassStyles.fill).toBe("#4c71f2");
|
||||
expect(queryByTestId(document.body, "card-border")).toHaveAttribute(
|
||||
"fill",
|
||||
"#FFFEFE"
|
||||
);
|
||||
});
|
||||
|
||||
it("should render custom colors properly", () => {
|
||||
const customColors = {
|
||||
title_color: "5a0",
|
||||
icon_color: "1b998b",
|
||||
text_color: "9991",
|
||||
bg_color: "252525",
|
||||
};
|
||||
|
||||
document.body.innerHTML = renderStatsCard(stats, { ...customColors });
|
||||
|
||||
const styleTag = document.querySelector("style");
|
||||
const stylesObject = cssToObject(styleTag.innerHTML);
|
||||
|
||||
const headerClassStyles = stylesObject[".header"];
|
||||
const statClassStyles = stylesObject[".stat"];
|
||||
const iconClassStyles = stylesObject[".icon"];
|
||||
|
||||
expect(headerClassStyles.fill).toBe(`#${customColors.title_color}`);
|
||||
expect(statClassStyles.fill).toBe(`#${customColors.text_color}`);
|
||||
expect(iconClassStyles.fill).toBe(`#${customColors.icon_color}`);
|
||||
expect(queryByTestId(document.body, "card-border")).toHaveAttribute(
|
||||
"fill",
|
||||
"#252525"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user