Compare commits
93
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6aa462881 | ||
|
|
be79fe4051 | ||
|
|
b039fa16af | ||
|
|
61442cb359 | ||
|
|
431f2a0b5b | ||
|
|
2709d5b434 | ||
|
|
96f89ad2b7 | ||
|
|
7dcb60f576 | ||
|
|
d5f410d4ec | ||
|
|
1c0332352a | ||
|
|
4febc6644a | ||
|
|
6fedd69f86 | ||
|
|
1078c85455 | ||
|
|
9e4d83247e | ||
|
|
597dac2985 | ||
|
|
0ea2668176 | ||
|
|
1091fc7695 | ||
|
|
6d4fbfecfb | ||
|
|
331295139d | ||
|
|
a726e8dbaf | ||
|
|
817f736b4e | ||
|
|
987fb0c30d | ||
|
|
96d37c706f | ||
|
|
3a24ad62b1 | ||
|
|
989079e4fd | ||
|
|
20bddc3a0c | ||
|
|
95f1233ab9 | ||
|
|
1a53fb9a84 | ||
|
|
19bacf780a | ||
|
|
89bb8fea42 | ||
|
|
0bade9be77 | ||
|
|
5fce46d67c | ||
|
|
00ce9d37d2 | ||
|
|
9328a57ea6 | ||
|
|
579ed2bab8 | ||
|
|
429d65a52b | ||
|
|
3d8dea90e2 | ||
|
|
47370adbbb | ||
|
|
2efb399f33 | ||
|
|
dcde5f8205 | ||
|
|
3b3ce41ae4 | ||
|
|
e3519ceb4c | ||
|
|
c297798889 | ||
|
|
6059cb1691 | ||
|
|
2658a8737d | ||
|
|
cbd0e583ff | ||
|
|
6f79b495a6 | ||
|
|
014f710e12 | ||
|
|
29d3ea63f8 | ||
|
|
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 | ||
|
|
4488e9292b | ||
|
|
3180144fa4 | ||
|
|
61a2866aff | ||
|
|
7e3278c708 | ||
|
|
45941f6c70 | ||
|
|
223bd659ab | ||
|
|
c072662cda | ||
|
|
12fd842fce | ||
|
|
137a9f2038 | ||
|
|
9083ae3e49 | ||
|
|
43c49de37f | ||
|
|
4292212b30 | ||
|
|
6f6a7f2763 | ||
|
|
a27d977c7d | ||
|
|
86594ee1b8 | ||
|
|
8d982d59c6 | ||
|
|
2f91bd5222 | ||
|
|
6b04481c81 |
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: ["https://www.paypal.me/anuraghazra"] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots / Live demo link (paste the github-readme-stats link as markdown image)**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "*"
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: "12.x"
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-npm-cache-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-npm-cache-
|
||||
|
||||
- name: Install & Test
|
||||
run: |
|
||||
npm install
|
||||
npm run test
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
.vercel
|
||||
.env
|
||||
node_modules
|
||||
node_modules
|
||||
package-lock.json
|
||||
+2
-1
@@ -1 +1,2 @@
|
||||
.env
|
||||
.env
|
||||
package-lock.json
|
||||
@@ -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.
|
||||
+34
-138
@@ -1,149 +1,45 @@
|
||||
const axios = require("axios");
|
||||
require("dotenv").config();
|
||||
|
||||
async function fetchStats(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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
login: username,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const stats = {
|
||||
name: "",
|
||||
totalPRs: 0,
|
||||
totalCommits: 0,
|
||||
totalIssues: 0,
|
||||
totalStars: 0,
|
||||
contributedTo: 0,
|
||||
};
|
||||
if (res.data.error) return stats;
|
||||
|
||||
const user = res.data.data.user;
|
||||
|
||||
stats.name = user.name;
|
||||
stats.totalIssues = user.issues.totalCount;
|
||||
stats.totalCommits = user.contributionsCollection.totalCommitContributions;
|
||||
stats.totalPRs = user.pullRequests.totalCount;
|
||||
stats.contributedTo = user.repositoriesContributedTo.totalCount;
|
||||
|
||||
stats.totalStars = user.repositories.nodes.reduce((prev, curr) => {
|
||||
return prev + curr.stargazers.totalCount;
|
||||
}, 0);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
const createTextNode = (icon, label, value, lheight) => {
|
||||
return `
|
||||
<tspan x="25" dy="${lheight}" class="stat bold">
|
||||
<tspan class="icon ${
|
||||
icon === "★" && "star-icon"
|
||||
}" fill="#4C71F2">${icon}</tspan> ${label}:</tspan>
|
||||
<tspan x="155" dy="0" class="stat">${value}</tspan>
|
||||
`;
|
||||
};
|
||||
|
||||
const renderSVG = (stats, options) => {
|
||||
const {
|
||||
name,
|
||||
totalStars,
|
||||
totalCommits,
|
||||
totalIssues,
|
||||
totalPRs,
|
||||
contributedTo,
|
||||
} = stats;
|
||||
const { hide, show_icons, hide_border, line_height } = options || {};
|
||||
|
||||
const lheight = line_height || 25;
|
||||
|
||||
const STAT_MAP = {
|
||||
stars: createTextNode("★", "Total Stars", totalStars, lheight),
|
||||
commits: createTextNode("🕗", "Total Commits", totalCommits, lheight),
|
||||
prs: createTextNode("🔀", "Total PRs", totalPRs, lheight),
|
||||
issues: createTextNode("ⓘ", "Total Issues", totalIssues, lheight),
|
||||
contribs: createTextNode("📕", "Contributed to", contributedTo, lheight),
|
||||
};
|
||||
|
||||
const statItems = Object.keys(STAT_MAP)
|
||||
.filter((key) => !hide.includes(key))
|
||||
.map((key) => STAT_MAP[key]);
|
||||
|
||||
const height = 45 + (statItems.length + 1) * lheight;
|
||||
|
||||
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 }
|
||||
.star-icon { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; }
|
||||
.bold { font-weight: 700 }
|
||||
.icon {
|
||||
display: none;
|
||||
${!!show_icons && "display: block"}
|
||||
}
|
||||
</style>
|
||||
${
|
||||
!hide_border &&
|
||||
`<rect x="0.5" y="0.5" width="494" height="99%" rx="4.5" fill="#FFFEFE" stroke="#E4E2E2"/>`
|
||||
}
|
||||
|
||||
<text x="25" y="35" class="header">${name}'s GitHub Stats</text>
|
||||
<text y="45">
|
||||
${statItems}
|
||||
</text>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
const { renderError, parseBoolean } = require("../src/utils");
|
||||
const fetchStats = require("../src/fetchStats");
|
||||
const renderStatsCard = require("../src/renderStatsCard");
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
const username = req.query.username;
|
||||
const hide = req.query.hide;
|
||||
const hide_border = req.query.hide_border;
|
||||
const show_icons = req.query.show_icons;
|
||||
const line_height = req.query.line_height;
|
||||
|
||||
const stats = await fetchStats(username);
|
||||
const {
|
||||
username,
|
||||
hide,
|
||||
hide_title,
|
||||
hide_border,
|
||||
hide_rank,
|
||||
show_icons,
|
||||
line_height,
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
} = req.query;
|
||||
let stats;
|
||||
|
||||
res.setHeader("Cache-Control", "public, max-age=1800");
|
||||
res.setHeader("Content-Type", "image/svg+xml");
|
||||
|
||||
try {
|
||||
stats = await fetchStats(username);
|
||||
} catch (err) {
|
||||
return res.send(renderError(err.message));
|
||||
}
|
||||
|
||||
res.send(
|
||||
renderSVG(stats, {
|
||||
renderStatsCard(stats, {
|
||||
hide: JSON.parse(hide || "[]"),
|
||||
show_icons,
|
||||
hide_border,
|
||||
show_icons: parseBoolean(show_icons),
|
||||
hide_title: parseBoolean(hide_title),
|
||||
hide_border: parseBoolean(hide_border),
|
||||
hide_rank: parseBoolean(hide_rank),
|
||||
line_height,
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
+31
-118
@@ -1,125 +1,38 @@
|
||||
const axios = require("axios");
|
||||
require("dotenv").config();
|
||||
|
||||
function kFormatter(num) {
|
||||
return Math.abs(num) > 999
|
||||
? Math.sign(num) * (Math.abs(num) / 1000).toFixed(1) + "k"
|
||||
: Math.sign(num) * Math.abs(num);
|
||||
}
|
||||
|
||||
async function fetchRepo(username, reponame) {
|
||||
const res = await axios({
|
||||
url: "https://api.github.com/graphql",
|
||||
method: "post",
|
||||
headers: {
|
||||
Authorization: `bearer ${process.env.GITHUB_TOKEN}`,
|
||||
},
|
||||
data: {
|
||||
query: `
|
||||
query getRepo($login: String!, $repo: String!) {
|
||||
user(login: $login) {
|
||||
repository(name: $repo) {
|
||||
name
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
description
|
||||
primaryLanguage {
|
||||
color
|
||||
id
|
||||
name
|
||||
}
|
||||
forkCount
|
||||
}
|
||||
}
|
||||
organization(login: $login) {
|
||||
repository(name: $repo) {
|
||||
name
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
description
|
||||
primaryLanguage {
|
||||
color
|
||||
id
|
||||
name
|
||||
}
|
||||
forkCount
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
login: username,
|
||||
repo: reponame,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (res.data.error && res.data.error.type !== "NOT_FOUND") return {};
|
||||
if (!res.data.data.organization && res.data.data.user)
|
||||
return res.data.data.user.repository;
|
||||
|
||||
const isOrg = res.data.data.organization && !res.data.data.user;
|
||||
if (isOrg) return res.data.data.organization.repository;
|
||||
|
||||
return res.data.data.user.repository;
|
||||
}
|
||||
|
||||
const renderRepoCard = (repo) => {
|
||||
const { name, description, primaryLanguage, stargazers, forkCount } = repo;
|
||||
const height = 120;
|
||||
|
||||
const shiftText = primaryLanguage.name.length > 15 ? 0 : 30;
|
||||
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 }
|
||||
</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">
|
||||
<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>
|
||||
|
||||
<text x="50" y="38" class="header">${name}</text>
|
||||
<text class="description" x="25" y="70">${
|
||||
description ? description.slice(0, 60) : "No description provided"
|
||||
}..</text>
|
||||
|
||||
|
||||
<g transform="translate(30, 100)">
|
||||
<circle cx="0" cy="-5" r="6" fill="${primaryLanguage.color}" />
|
||||
<text class="gray" x="15">${primaryLanguage.name}</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(${155 - shiftText}, 100)">
|
||||
<svg y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16" fill="#586069">
|
||||
<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 class="gray" x="25">${kFormatter(stargazers.totalCount)}</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">
|
||||
<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 class="gray" x="25">${kFormatter(forkCount)}</text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
const { renderError, parseBoolean } = require("../src/utils");
|
||||
const fetchRepo = require("../src/fetchRepo");
|
||||
const renderRepoCard = require("../src/renderRepoCard");
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
const username = req.query.username;
|
||||
const repo = req.query.repo;
|
||||
const {
|
||||
username,
|
||||
repo,
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
show_owner,
|
||||
} = req.query;
|
||||
|
||||
const repoData = await fetchRepo(username, repo);
|
||||
let repoData;
|
||||
|
||||
res.setHeader("Cache-Control", "public, max-age=1800");
|
||||
res.setHeader("Content-Type", "image/svg+xml");
|
||||
res.send(renderRepoCard(repoData));
|
||||
|
||||
try {
|
||||
repoData = await fetchRepo(username, repo);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return res.send(renderError(err.message));
|
||||
}
|
||||
|
||||
res.send(
|
||||
renderRepoCard(repoData, {
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
show_owner: parseBoolean(show_owner),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
clearMocks: true,
|
||||
};
|
||||
Generated
-46
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"name": "github-readme-stats",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"axios": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
|
||||
"integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"follow-redirects": "1.5.10"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
|
||||
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
|
||||
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "=3.1.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-3
@@ -1,15 +1,21 @@
|
||||
{
|
||||
"name": "github-readme-stats",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"description": "Dynamically generate stats for your github readmes",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"author": "Anurag Hazra",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"axios": "^0.19.2"
|
||||
"@testing-library/dom": "^7.20.0",
|
||||
"@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": {
|
||||
"dotenv": "^8.2.0"
|
||||
|
||||
@@ -1,25 +1,53 @@
|
||||
## Github Readme stats
|
||||
<p align="center">
|
||||
<img width="100px" src="https://res.cloudinary.com/anuraghazra/image/upload/v1594908242/logo_ccswme.svg" align="center" alt="Github Readme Stats" />
|
||||
<h2 align="center">GitHub Readme Stats</h2>
|
||||
<p align="center">Get dynamically generated GitHub stats on your readmes!</p>
|
||||
</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>
|
||||
<p align="center">Loved the project? Please consider <a href="https://www.paypal.me/anuraghazra">donating</a> to help it improve!
|
||||
|
||||
# Features
|
||||
|
||||
- [Github Stats Card](#github-stats-card)
|
||||
- [Github Extra Pins](#github-extra-pins)
|
||||
- [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
|
||||
# GitHub Stats Card
|
||||
|
||||
Copy paste this into your markdown content and thats it. simple!
|
||||
Copy paste this into your markdown content, and that's it. Simple!
|
||||
|
||||
change the `?username=` value to your GitHubs's username
|
||||
Change the `?username=` value to your GitHub's username.
|
||||
|
||||
```md
|
||||

|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Hiding certain stats
|
||||
_Note: Ranks are calculated based on user's stats, see [src/calculateRank.js](./src/calculateRank.js)_
|
||||
|
||||
To hide any specific stats you can pass a query parameter `?hide=` with an array of items you wanna hide.
|
||||
### Hiding individual stats
|
||||
|
||||
To hide any specific stats, you can pass a query parameter `?hide=` with an array of items you wanna hide.
|
||||
|
||||
> Options: `&hide=["stars","prs","issues","contribs"]`
|
||||
|
||||
@@ -29,16 +57,36 @@ To hide any specific stats you can pass a query parameter `?hide=` with an array
|
||||
|
||||
### Showing icons
|
||||
|
||||
To enable icons you can pass `show_icons=true` in the query param like so
|
||||
To enable icons, you can pass `show_icons=true` in the query param, like so:
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
Other options:
|
||||
### Customization
|
||||
|
||||
- `&hide_border=true` hide the border box if you don't like it :D.
|
||||
- `&line_height=30` control the line-height between text.
|
||||
You can customize the appearance of your `Stats Card` or `Repo Card` however you want with URL params.
|
||||
|
||||
Customization Options:
|
||||
|
||||
| Option | type | description | Stats Card (default) | Repo Card (default) |
|
||||
| ----------- | --------- | ------------------------------------ | -------------------- | ------------------- |
|
||||
| title_color | hex color | title color | 2f80ed | 2f80ed |
|
||||
| text_color | hex color | body color | 333 | 333 |
|
||||
| icon_color | hex color | icon color | 4c71f2 | 586069 |
|
||||
| bg_color | hex color | card bg color | FFFEFE | FFFEFE |
|
||||
| line_height | number | control the line-height between text | 30 | N/A |
|
||||
| hide_rank | boolean | hides the ranking | false | N/A |
|
||||
| hide_title | boolean | hides the stats title | false | N/A |
|
||||
| hide_border | boolean | hides the stats card border | false | N/A |
|
||||
| show_owner | boolean | shows owner name in repo card | N/A | false |
|
||||
| show_icons | boolean | shows icons | false | N/A |
|
||||
|
||||
- You can also customize the cards to be compatible with dark mode
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### Demo
|
||||
|
||||
@@ -54,15 +102,23 @@ Other options:
|
||||
|
||||

|
||||
|
||||
# Github Extra Pins
|
||||
- Customizing stats card
|
||||
|
||||
Github extra pins allows you to pin more than 6 repositories in your profile by using github readme profile.
|
||||

|
||||
|
||||
Yey! you are no longer limited to 6 pinned repos
|
||||
- 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.
|
||||
|
||||
Yey! You are no longer limited to 6 pinned repositories.
|
||||
|
||||
### Usage
|
||||
|
||||
Copy paste this code into your readme and change the links.
|
||||
Copy-paste this code into your readme and change the links.
|
||||
|
||||
Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`
|
||||
|
||||
@@ -74,9 +130,13 @@ Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
Use [show_owner](#customization) variable to include the repo's owner username
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
### Quick Tip (Align The Repo Cards)
|
||||
|
||||
Normally you won't be able to layout the images side by side to do that you can use this approach
|
||||
You usually won't be able to layout the images side by side. To do that you can use this approach:
|
||||
|
||||
```md
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats">
|
||||
@@ -87,6 +147,49 @@ Normally you won't be able to layout the images side by side to do that you can
|
||||
</a>
|
||||
```
|
||||
|
||||
## Deploy on your own Vercel instance
|
||||
|
||||
Since the GitHub API only allows 5k requests per hour, it is possible that my `https://github-readme-stats.vercel.app/api` could hit the rate limiter. If you host it on your own Vercel server, then you don't have to worry about anything. Click on the deploy button to get started!
|
||||
|
||||
NOTE: Since [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) we should be able to handle more than 5k requests and have no issues with downtime :D
|
||||
|
||||
[](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
<details>
|
||||
<summary>Guide on setting up Vercel</summary>
|
||||
|
||||
1. Go to [vercel.com](https://vercel.com/)
|
||||
1. Click on `Log in`
|
||||

|
||||
1. Sign in with GitHub by pressing `Continue with GitHub`
|
||||

|
||||
1. Sign into GitHub and allow access to all repositories, if prompted
|
||||
1. Fork this repo
|
||||
1. Go back to your [Vercel dashboard](https://vercel.com/dashboard)
|
||||
1. Select `Import Project`
|
||||

|
||||
1. Select `Import Git Repository`
|
||||

|
||||
1. Select root and keep everything as is, just add your environment variable named PAT_1 (as shown), which will contain a personal access token (PAT), which you can easily create [here](https://github.com/settings/tokens/new) (leave everything as is, just name it something, it can be anything you want)
|
||||

|
||||
1. Click deploy, and you're good to go. See your domains to use the API!
|
||||
</details>
|
||||
|
||||
## :sparkling_heart: Support the project
|
||||
|
||||
I open-source almost everything I can, and I try to reply to everyone needing help using these projects. Obviously,
|
||||
this takes time. You can use this service for free.
|
||||
|
||||
However, if you are using this project and happy with it or just want to encourage me to continue creating stuff, there are few ways you can do it :-
|
||||
|
||||
- Giving proper credit when you use github-readme-stats on your readme, linking back to it :D
|
||||
- Starring and sharing the project :rocket:
|
||||
- [](https://www.paypal.me/anuraghazra) - You can make one-time donations via PayPal. I'll probably buy a ~~coffee~~ tea. :tea:
|
||||
|
||||
Thanks! :heart:
|
||||
|
||||
---
|
||||
|
||||
Contributions are welcomed! <3
|
||||
|
||||
Made with :heart: and javascript.
|
||||
Made with :heart: and JavaScript.
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// https://stackoverflow.com/a/5263759/10629172
|
||||
function normalcdf(mean, sigma, to) {
|
||||
var z = (to - mean) / Math.sqrt(2 * sigma * sigma);
|
||||
var t = 1 / (1 + 0.3275911 * Math.abs(z));
|
||||
var a1 = 0.254829592;
|
||||
var a2 = -0.284496736;
|
||||
var a3 = 1.421413741;
|
||||
var a4 = -1.453152027;
|
||||
var a5 = 1.061405429;
|
||||
var erf =
|
||||
1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-z * z);
|
||||
var sign = 1;
|
||||
if (z < 0) {
|
||||
sign = -1;
|
||||
}
|
||||
return (1 / 2) * (1 + sign * erf);
|
||||
}
|
||||
|
||||
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 REPO_OFFSET = 1;
|
||||
|
||||
const ALL_OFFSETS =
|
||||
CONTRIBS_OFFSET +
|
||||
ISSUES_OFFSET +
|
||||
STARS_OFFSET +
|
||||
PRS_OFFSET +
|
||||
FOLLOWERS_OFFSET +
|
||||
REPO_OFFSET;
|
||||
|
||||
const RANK_S_VALUE = 1;
|
||||
const RANK_DOUBLE_A_VALUE = 25;
|
||||
const RANK_A2_VALUE = 45;
|
||||
const RANK_A3_VALUE = 60;
|
||||
const RANK_B_VALUE = 100;
|
||||
|
||||
const TOTAL_VALUES =
|
||||
RANK_S_VALUE + RANK_A2_VALUE + RANK_A3_VALUE + RANK_B_VALUE;
|
||||
|
||||
// prettier-ignore
|
||||
const score = (
|
||||
totalCommits * COMMITS_OFFSET +
|
||||
contributions * CONTRIBS_OFFSET +
|
||||
issues * ISSUES_OFFSET +
|
||||
stargazers * STARS_OFFSET +
|
||||
prs * PRS_OFFSET +
|
||||
followers * FOLLOWERS_OFFSET +
|
||||
totalRepos * REPO_OFFSET
|
||||
) / 100;
|
||||
|
||||
const normalizedScore = normalcdf(score, TOTAL_VALUES, ALL_OFFSETS) * 100;
|
||||
|
||||
let level = "";
|
||||
|
||||
if (normalizedScore < RANK_S_VALUE) {
|
||||
level = "S+";
|
||||
}
|
||||
if (
|
||||
normalizedScore >= RANK_S_VALUE &&
|
||||
normalizedScore < RANK_DOUBLE_A_VALUE
|
||||
) {
|
||||
level = "S";
|
||||
}
|
||||
if (
|
||||
normalizedScore >= RANK_DOUBLE_A_VALUE &&
|
||||
normalizedScore < RANK_A2_VALUE
|
||||
) {
|
||||
level = "A++";
|
||||
}
|
||||
if (normalizedScore >= RANK_A2_VALUE && normalizedScore < RANK_A3_VALUE) {
|
||||
level = "A+";
|
||||
}
|
||||
if (normalizedScore >= RANK_A3_VALUE && normalizedScore < RANK_B_VALUE) {
|
||||
level = "B+";
|
||||
}
|
||||
|
||||
return { level, score: normalizedScore };
|
||||
}
|
||||
|
||||
module.exports = calculateRank;
|
||||
@@ -0,0 +1,79 @@
|
||||
const { request } = require("./utils");
|
||||
const retryer = require("./retryer");
|
||||
|
||||
const fetcher = (variables, token) => {
|
||||
return request(
|
||||
{
|
||||
query: `
|
||||
fragment RepoInfo on Repository {
|
||||
name
|
||||
nameWithOwner
|
||||
isPrivate
|
||||
isArchived
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables,
|
||||
},
|
||||
{
|
||||
Authorization: `bearer ${token}`,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
async function fetchRepo(username, reponame) {
|
||||
if (!username || !reponame) {
|
||||
throw new Error("Invalid username or reponame");
|
||||
}
|
||||
|
||||
let res = await retryer(fetcher, { login: username, repo: reponame });
|
||||
|
||||
const data = res.data.data;
|
||||
|
||||
if (!data.user && !data.organization) {
|
||||
throw new Error("Not found");
|
||||
}
|
||||
|
||||
const isUser = data.organization === null && data.user;
|
||||
const isOrg = data.user === null && data.organization;
|
||||
|
||||
if (isUser) {
|
||||
if (!data.user.repository || data.user.repository.isPrivate) {
|
||||
throw new Error("User Repository Not found");
|
||||
}
|
||||
return data.user.repository;
|
||||
}
|
||||
|
||||
if (isOrg) {
|
||||
if (
|
||||
!data.organization.repository ||
|
||||
data.organization.repository.isPrivate
|
||||
) {
|
||||
throw new Error("Organization Repository Not found");
|
||||
}
|
||||
return data.organization.repository;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = fetchRepo;
|
||||
@@ -0,0 +1,93 @@
|
||||
const { request } = require("./utils");
|
||||
const retryer = require("./retryer");
|
||||
const calculateRank = require("./calculateRank");
|
||||
require("dotenv").config();
|
||||
|
||||
const fetcher = (variables, token) => {
|
||||
return request(
|
||||
{
|
||||
query: `
|
||||
query userInfo($login: String!) {
|
||||
user(login: $login) {
|
||||
name
|
||||
login
|
||||
contributionsCollection {
|
||||
totalCommitContributions
|
||||
}
|
||||
repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
|
||||
totalCount
|
||||
}
|
||||
pullRequests(first: 1) {
|
||||
totalCount
|
||||
}
|
||||
issues(first: 1) {
|
||||
totalCount
|
||||
}
|
||||
followers {
|
||||
totalCount
|
||||
}
|
||||
repositories(first: 100, ownerAffiliations: OWNER, isFork: false, orderBy: {direction: DESC, field: STARGAZERS}) {
|
||||
totalCount
|
||||
nodes {
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables,
|
||||
},
|
||||
{
|
||||
Authorization: `bearer ${token}`,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
async function fetchStats(username) {
|
||||
if (!username) throw Error("Invalid username");
|
||||
|
||||
const stats = {
|
||||
name: "",
|
||||
totalPRs: 0,
|
||||
totalCommits: 0,
|
||||
totalIssues: 0,
|
||||
totalStars: 0,
|
||||
contributedTo: 0,
|
||||
rank: { level: "C", score: 0 },
|
||||
};
|
||||
|
||||
let res = await retryer(fetcher, { login: username });
|
||||
|
||||
if (res.data.errors) {
|
||||
console.log(res.data.errors);
|
||||
throw Error(res.data.errors[0].message || "Could not fetch user");
|
||||
}
|
||||
|
||||
const user = res.data.data.user;
|
||||
|
||||
stats.name = user.name || user.login;
|
||||
stats.totalIssues = user.issues.totalCount;
|
||||
stats.totalCommits = user.contributionsCollection.totalCommitContributions;
|
||||
stats.totalPRs = user.pullRequests.totalCount;
|
||||
stats.contributedTo = user.repositoriesContributedTo.totalCount;
|
||||
|
||||
stats.totalStars = user.repositories.nodes.reduce((prev, curr) => {
|
||||
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;
|
||||
}
|
||||
|
||||
module.exports = fetchStats;
|
||||
@@ -0,0 +1,94 @@
|
||||
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 }
|
||||
.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;
|
||||
@@ -0,0 +1,11 @@
|
||||
const icons = {
|
||||
star: `<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"/>`,
|
||||
commits: `<path fill-rule="evenodd" d="M1.643 3.143L.427 1.927A.25.25 0 000 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 00.177-.427L2.715 4.215a6.5 6.5 0 11-1.18 4.458.75.75 0 10-1.493.154 8.001 8.001 0 101.6-5.684zM7.75 4a.75.75 0 01.75.75v2.992l2.028.812a.75.75 0 01-.557 1.392l-2.5-1A.75.75 0 017 8.25v-3.5A.75.75 0 017.75 4z"/>`,
|
||||
prs: `<path fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"/>`,
|
||||
issues: `<path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"/>`,
|
||||
icon: `<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"/>`,
|
||||
contribs: `<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"/>`,
|
||||
fork: `<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>`,
|
||||
};
|
||||
|
||||
module.exports = icons;
|
||||
@@ -0,0 +1,87 @@
|
||||
const { kFormatter, encodeHTML, fallbackColor } = require("../src/utils");
|
||||
const icons = require("./icons");
|
||||
|
||||
const renderRepoCard = (repo, options = {}) => {
|
||||
const {
|
||||
name,
|
||||
nameWithOwner,
|
||||
description,
|
||||
primaryLanguage,
|
||||
stargazers,
|
||||
isArchived,
|
||||
forkCount,
|
||||
} = repo;
|
||||
const { title_color, icon_color, text_color, bg_color, show_owner } = options;
|
||||
|
||||
const header = show_owner ? nameWithOwner : name;
|
||||
const langName = primaryLanguage ? primaryLanguage.name : "Unspecified";
|
||||
const langColor = primaryLanguage ? primaryLanguage.color : "#333";
|
||||
|
||||
const height = 120;
|
||||
const shiftText = langName.length > 15 ? 0 : 30;
|
||||
|
||||
let desc = description || "No description provided";
|
||||
if (desc.length > 55) {
|
||||
desc = `${description.slice(0, 55)}..`;
|
||||
}
|
||||
|
||||
const titleColor = fallbackColor(title_color, "#2f80ed");
|
||||
const iconColor = fallbackColor(icon_color, "#586069");
|
||||
const textColor = fallbackColor(text_color, "#333");
|
||||
const bgColor = fallbackColor(bg_color, "#FFFEFE");
|
||||
|
||||
const totalStars = kFormatter(stargazers.totalCount);
|
||||
const totalForks = kFormatter(forkCount);
|
||||
|
||||
const archiveBadge = isArchived
|
||||
? `
|
||||
<g data-testid="archive-badge" class="archive-badge" transform="translate(320, 38)">
|
||||
<rect stroke="${textColor}" stroke-width="1" width="70" height="20" x="-12" y="-14" ry="10" rx="10"></rect>
|
||||
<text fill="${textColor}">Archived</text>
|
||||
</g>
|
||||
`
|
||||
: "";
|
||||
|
||||
return `
|
||||
<svg version="1.1" 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: ${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} }
|
||||
.archive-badge { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; }
|
||||
.archive-badge rect { opacity: 0.2 }
|
||||
</style>
|
||||
<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">
|
||||
${icons.contribs}
|
||||
</svg>
|
||||
|
||||
${archiveBadge}
|
||||
|
||||
<text x="50" y="38" class="header">${header}</text>
|
||||
<text class="description" x="25" y="70">${encodeHTML(desc)}</text>
|
||||
|
||||
<g transform="translate(30, 100)">
|
||||
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="${langColor}" />
|
||||
<text data-testid="lang" class="gray" x="15">${langName}</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(${155 - shiftText}, 100)">
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
${icons.star}
|
||||
</svg>
|
||||
<text data-testid="stargazers" class="gray" x="25">${totalStars}</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(${220 - shiftText}, 100)">
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
${icons.fork}
|
||||
</svg>
|
||||
<text data-testid="forkcount" class="gray" x="25">${totalForks}</text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
|
||||
module.exports = renderRepoCard;
|
||||
@@ -0,0 +1,199 @@
|
||||
const { kFormatter, fallbackColor } = require("../src/utils");
|
||||
const getStyles = require("./getStyles");
|
||||
const icons = require("./icons");
|
||||
|
||||
const createTextNode = ({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
id,
|
||||
index,
|
||||
lineHeight,
|
||||
showIcons,
|
||||
}) => {
|
||||
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);
|
||||
const translateY = lheight - lineHeight / 2;
|
||||
const labelOffset = showIcons ? `x="25"` : "";
|
||||
const iconSvg = showIcons
|
||||
? `
|
||||
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
${icon}
|
||||
</svg>
|
||||
`
|
||||
: "";
|
||||
return `
|
||||
<g class="stagger" style="animation-delay: ${staggerDelay}ms" transform="translate(25, ${translateY})">
|
||||
${iconSvg}
|
||||
<text class="stat bold" ${labelOffset} y="12.5">${label}:</text>
|
||||
<text class="stat" x="135" y="12.5" data-testid="${id}">${kValue}</text>
|
||||
</g>
|
||||
`;
|
||||
};
|
||||
|
||||
const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
const {
|
||||
name,
|
||||
totalStars,
|
||||
totalCommits,
|
||||
totalIssues,
|
||||
totalPRs,
|
||||
contributedTo,
|
||||
rank,
|
||||
} = stats;
|
||||
const {
|
||||
hide = [],
|
||||
show_icons = false,
|
||||
hide_title = 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 = fallbackColor(title_color, "#2f80ed");
|
||||
const iconColor = fallbackColor(icon_color, "#4c71f2");
|
||||
const textColor = fallbackColor(text_color, "#333");
|
||||
const bgColor = fallbackColor(bg_color, "#FFFEFE");
|
||||
|
||||
// Meta data for creating text nodes with createTextNode function
|
||||
const STATS = {
|
||||
stars: {
|
||||
icon: icons.star,
|
||||
label: "Total Stars",
|
||||
value: totalStars,
|
||||
id: "stars",
|
||||
},
|
||||
commits: {
|
||||
icon: icons.commits,
|
||||
label: "Total Commits",
|
||||
value: totalCommits,
|
||||
id: "commits",
|
||||
},
|
||||
prs: {
|
||||
icon: icons.prs,
|
||||
label: "Total PRs",
|
||||
value: totalPRs,
|
||||
id: "prs",
|
||||
},
|
||||
issues: {
|
||||
icon: icons.issues,
|
||||
label: "Total Issues",
|
||||
value: totalIssues,
|
||||
id: "issues",
|
||||
},
|
||||
contribs: {
|
||||
icon: icons.contribs,
|
||||
label: "Contributed to",
|
||||
value: contributedTo,
|
||||
id: "contribs",
|
||||
},
|
||||
};
|
||||
|
||||
// filter out hidden stats defined by user & create the text nodes
|
||||
const statItems = Object.keys(STATS)
|
||||
.filter((key) => !hide.includes(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,
|
||||
showIcons: show_icons,
|
||||
})
|
||||
);
|
||||
|
||||
// Calculate the card height depending on how many items there are
|
||||
// but if rank circle is visible clamp the minimum height to `150`
|
||||
let height = Math.max(
|
||||
45 + (statItems.length + 1) * lheight,
|
||||
hide_rank ? 0 : 150
|
||||
);
|
||||
|
||||
// the better user's score the the rank will be closer to zero so
|
||||
// subtracting 100 to get the progress in 100%
|
||||
const progress = 100 - rank.score;
|
||||
|
||||
const styles = getStyles({
|
||||
titleColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
show_icons,
|
||||
progress,
|
||||
});
|
||||
|
||||
// Conditionally rendered elements
|
||||
const title = hide_title
|
||||
? ""
|
||||
: `<text x="25" y="35" class="header">${name}'s GitHub Stats</text>`;
|
||||
|
||||
const border = hide_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" />
|
||||
<g class="rank-text">
|
||||
<text
|
||||
x="${rank.level.length === 1 ? "-4" : "0"}"
|
||||
y="0"
|
||||
alignment-baseline="central"
|
||||
dominant-baseline="central"
|
||||
text-anchor="middle"
|
||||
>
|
||||
${rank.level}
|
||||
</text>
|
||||
</g>
|
||||
</g>`;
|
||||
|
||||
if (hide_title) {
|
||||
height -= 30;
|
||||
}
|
||||
|
||||
return `
|
||||
<svg width="495" height="${height}" viewBox="0 0 495 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
${styles}
|
||||
</style>
|
||||
|
||||
${border}
|
||||
${title}
|
||||
|
||||
<g data-testid="card-body-content" transform="translate(0, ${
|
||||
hide_title ? -30 : 0
|
||||
})">
|
||||
${rankCircle}
|
||||
|
||||
<svg x="0" y="45">
|
||||
${statItems.toString().replace(/\,/gm, "")}
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
|
||||
module.exports = renderStatsCard;
|
||||
@@ -0,0 +1,43 @@
|
||||
const retryer = async (fetcher, variables, retries = 0) => {
|
||||
if (retries > 7) {
|
||||
throw new Error("Maximum retries exceeded");
|
||||
}
|
||||
try {
|
||||
console.log(`Trying PAT_${retries + 1}`);
|
||||
|
||||
// try to fetch with the first token since RETRIES is 0 index i'm adding +1
|
||||
let response = await fetcher(
|
||||
variables,
|
||||
process.env[`PAT_${retries + 1}`],
|
||||
retries
|
||||
);
|
||||
|
||||
// prettier-ignore
|
||||
const isRateExceeded = response.data.errors && response.data.errors[0].type === "RATE_LIMITED";
|
||||
|
||||
// if rate limit is hit increase the RETRIES and recursively call the retryer
|
||||
// with username, and current RETRIES
|
||||
if (isRateExceeded) {
|
||||
console.log(`PAT_${retries + 1} Failed`);
|
||||
retries++;
|
||||
// directly return from the function
|
||||
return retryer(fetcher, variables, retries);
|
||||
}
|
||||
|
||||
// finally return the response
|
||||
return response;
|
||||
} catch (err) {
|
||||
// prettier-ignore
|
||||
// also checking for bad credentials if any tokens gets invalidated
|
||||
const isBadCredential = err.response.data && err.response.data.message === "Bad credentials";
|
||||
|
||||
if (isBadCredential) {
|
||||
console.log(`PAT_${retries + 1} Failed`);
|
||||
retries++;
|
||||
// directly return from the function
|
||||
return retryer(fetcher, variables, retries);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = retryer;
|
||||
@@ -0,0 +1,67 @@
|
||||
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">
|
||||
<style>
|
||||
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2F80ED }
|
||||
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #252525 }
|
||||
</style>
|
||||
<rect x="0.5" y="0.5" width="494" height="99%" rx="4.5" fill="#FFFEFE" stroke="#E4E2E2"/>
|
||||
<text x="25" y="45" class="text">Something went wrong! file an issue at https://git.io/JJmN9</text>
|
||||
<text id="message" x="25" y="65" class="text small">${message}</text>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
|
||||
// https://stackoverflow.com/a/48073476/10629172
|
||||
function encodeHTML(str) {
|
||||
return str.replace(/[\u00A0-\u9999<>&](?!#)/gim, function (i) {
|
||||
return "&#" + i.charCodeAt(0) + ";";
|
||||
});
|
||||
}
|
||||
|
||||
function kFormatter(num) {
|
||||
return Math.abs(num) > 999
|
||||
? Math.sign(num) * (Math.abs(num) / 1000).toFixed(1) + "k"
|
||||
: Math.sign(num) * Math.abs(num);
|
||||
}
|
||||
|
||||
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 parseBoolean(value) {
|
||||
if (value === "true") {
|
||||
return true;
|
||||
} else if (value === "false") {
|
||||
return false;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackColor(color, fallbackColor) {
|
||||
return (isValidHexColor(color) && `#${color}`) || fallbackColor;
|
||||
}
|
||||
|
||||
function request(data, headers) {
|
||||
return axios({
|
||||
url: "https://api.github.com/graphql",
|
||||
method: "post",
|
||||
headers,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
renderError,
|
||||
kFormatter,
|
||||
encodeHTML,
|
||||
isValidHexColor,
|
||||
request,
|
||||
parseBoolean,
|
||||
fallbackColor,
|
||||
};
|
||||
@@ -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: "A+", score: 49.16605417270399 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
require("@testing-library/jest-dom");
|
||||
const axios = require("axios");
|
||||
const MockAdapter = require("axios-mock-adapter");
|
||||
const fetchRepo = require("../src/fetchRepo");
|
||||
|
||||
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 },
|
||||
organization: null,
|
||||
},
|
||||
};
|
||||
const data_org = {
|
||||
data: {
|
||||
user: null,
|
||||
organization: { repository: data_repo },
|
||||
},
|
||||
};
|
||||
|
||||
const mock = new MockAdapter(axios);
|
||||
|
||||
afterEach(() => {
|
||||
mock.reset();
|
||||
});
|
||||
|
||||
describe("Test fetchRepo", () => {
|
||||
it("should fetch correct user repo", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
|
||||
|
||||
let repo = await fetchRepo("anuraghazra", "convoychat");
|
||||
expect(repo).toStrictEqual(data_repo);
|
||||
});
|
||||
|
||||
it("should fetch correct org repo", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, data_org);
|
||||
|
||||
let repo = await fetchRepo("anuraghazra", "convoychat");
|
||||
expect(repo).toStrictEqual(data_repo);
|
||||
});
|
||||
|
||||
it("should throw error if user is found but repo is null", async () => {
|
||||
mock
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.reply(200, { data: { user: { repository: null }, organization: null } });
|
||||
|
||||
await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow(
|
||||
"User Repository Not found"
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error if org is found but repo is null", async () => {
|
||||
mock
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.reply(200, { data: { user: null, organization: { repository: null } } });
|
||||
|
||||
await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow(
|
||||
"Organization Repository Not found"
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error if both user & org data not found", async () => {
|
||||
mock
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.reply(200, { data: { user: null, organization: null } });
|
||||
|
||||
await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow(
|
||||
"Not found"
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error if repository is private", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, {
|
||||
data: {
|
||||
user: { repository: { ...data_repo, isPrivate: true } },
|
||||
organization: null,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow(
|
||||
"User Repository Not found"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
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: {
|
||||
user: {
|
||||
name: "Anurag Hazra",
|
||||
repositoriesContributedTo: { totalCount: 61 },
|
||||
contributionsCollection: { totalCommitContributions: 100 },
|
||||
pullRequests: { totalCount: 300 },
|
||||
issues: { totalCount: 200 },
|
||||
followers: { totalCount: 100 },
|
||||
repositories: {
|
||||
totalCount: 5,
|
||||
nodes: [
|
||||
{ stargazers: { totalCount: 100 } },
|
||||
{ stargazers: { totalCount: 100 } },
|
||||
{ stargazers: { totalCount: 100 } },
|
||||
{ stargazers: { totalCount: 50 } },
|
||||
{ stargazers: { totalCount: 50 } },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const error = {
|
||||
errors: [
|
||||
{
|
||||
type: "NOT_FOUND",
|
||||
path: ["user"],
|
||||
locations: [],
|
||||
message: "Could not resolve to a User with the login of 'noname'.",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mock = new MockAdapter(axios);
|
||||
|
||||
afterEach(() => {
|
||||
mock.reset();
|
||||
});
|
||||
|
||||
describe("Test fetchStats", () => {
|
||||
it("should fetch correct stats", async () => {
|
||||
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",
|
||||
totalCommits: 100,
|
||||
totalIssues: 200,
|
||||
totalPRs: 300,
|
||||
totalStars: 400,
|
||||
rank,
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw error", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, error);
|
||||
|
||||
await expect(fetchStats("anuraghazra")).rejects.toThrow(
|
||||
"Could not resolve to a User with the login of 'noname'."
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
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: {
|
||||
username: "anuraghazra",
|
||||
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",
|
||||
full_name: "1",
|
||||
},
|
||||
};
|
||||
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")
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
require("@testing-library/jest-dom");
|
||||
const cssToObject = require("css-to-object");
|
||||
const renderRepoCard = require("../src/renderRepoCard");
|
||||
|
||||
const { queryByTestId } = require("@testing-library/dom");
|
||||
|
||||
const data_repo = {
|
||||
repository: {
|
||||
nameWithOwner: "anuraghazra/convoychat",
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
describe("Test renderRepoCard", () => {
|
||||
it("should render correctly", () => {
|
||||
document.body.innerHTML = renderRepoCard(data_repo.repository);
|
||||
|
||||
const [header] = document.getElementsByClassName("header");
|
||||
|
||||
expect(header).toHaveTextContent("convoychat");
|
||||
expect(header).not.toHaveTextContent("anuraghazra");
|
||||
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
|
||||
"Help us take over the world! React + TS + GraphQL Chat .."
|
||||
);
|
||||
expect(queryByTestId(document.body, "stargazers")).toHaveTextContent("38k");
|
||||
expect(queryByTestId(document.body, "forkcount")).toHaveTextContent("100");
|
||||
expect(queryByTestId(document.body, "lang")).toHaveTextContent(
|
||||
"TypeScript"
|
||||
);
|
||||
expect(queryByTestId(document.body, "lang-color")).toHaveAttribute(
|
||||
"fill",
|
||||
"#2b7489"
|
||||
);
|
||||
});
|
||||
|
||||
it("should display username in title (full repo name)", () => {
|
||||
document.body.innerHTML = renderRepoCard(data_repo.repository, {
|
||||
show_owner: true,
|
||||
});
|
||||
expect(document.getElementsByClassName("header")[0]).toHaveTextContent(
|
||||
"anuraghazra/convoychat"
|
||||
);
|
||||
});
|
||||
|
||||
it("should trim description", () => {
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
description:
|
||||
"Very long long long long long long long long text it should trim it",
|
||||
});
|
||||
|
||||
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
|
||||
"Very long long long long long long long long text it sh.."
|
||||
);
|
||||
|
||||
// Should not trim
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
description: "Small text should not trim",
|
||||
});
|
||||
|
||||
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
|
||||
"Small text should not trim"
|
||||
);
|
||||
});
|
||||
|
||||
it("should shift the text position depending on language length", () => {
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
primaryLanguage: {
|
||||
...data_repo.repository.primaryLanguage,
|
||||
name: "Jupyter Notebook",
|
||||
},
|
||||
});
|
||||
|
||||
expect(document.getElementsByTagName("g")[1]).toHaveAttribute(
|
||||
"transform",
|
||||
"translate(155, 100)"
|
||||
);
|
||||
|
||||
// Small lang
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
primaryLanguage: {
|
||||
...data_repo.repository.primaryLanguage,
|
||||
name: "Ruby",
|
||||
},
|
||||
});
|
||||
|
||||
expect(document.getElementsByTagName("g")[1]).toHaveAttribute(
|
||||
"transform",
|
||||
"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"
|
||||
);
|
||||
});
|
||||
|
||||
it("should render archive badge if repo is archived", () => {
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
isArchived: true,
|
||||
});
|
||||
|
||||
expect(queryByTestId(document.body, "archive-badge")).toHaveTextContent(
|
||||
"Archived"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,167 @@
|
||||
require("@testing-library/jest-dom");
|
||||
const cssToObject = require("css-to-object");
|
||||
const renderStatsCard = require("../src/renderStatsCard");
|
||||
|
||||
const {
|
||||
getByTestId,
|
||||
queryByTestId,
|
||||
queryAllByTestId,
|
||||
} = require("@testing-library/dom");
|
||||
|
||||
describe("Test renderStatsCard", () => {
|
||||
const stats = {
|
||||
name: "Anurag Hazra",
|
||||
totalStars: 100,
|
||||
totalCommits: 200,
|
||||
totalIssues: 300,
|
||||
totalPRs: 400,
|
||||
contributedTo: 500,
|
||||
rank: { level: "A+", score: 40 },
|
||||
};
|
||||
|
||||
it("should render correctly", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats);
|
||||
|
||||
expect(document.getElementsByClassName("header")[0].textContent).toBe(
|
||||
"Anurag Hazra's GitHub Stats"
|
||||
);
|
||||
|
||||
expect(
|
||||
document.body.getElementsByTagName("svg")[0].getAttribute("height")
|
||||
).toBe("195");
|
||||
expect(getByTestId(document.body, "stars").textContent).toBe("100");
|
||||
expect(getByTestId(document.body, "commits").textContent).toBe("200");
|
||||
expect(getByTestId(document.body, "issues").textContent).toBe("300");
|
||||
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", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
hide: "['issues', 'prs', 'contribs']",
|
||||
});
|
||||
|
||||
expect(
|
||||
document.body.getElementsByTagName("svg")[0].getAttribute("height")
|
||||
).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();
|
||||
expect(queryByTestId(document.body, "prs")).toBeNull();
|
||||
expect(queryByTestId(document.body, "contribs")).toBeNull();
|
||||
});
|
||||
|
||||
it("should hide_border", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, { hide_border: true });
|
||||
|
||||
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");
|
||||
console.log(styleTag.textContent);
|
||||
const stylesObject = cssToObject(styleTag.textContent);
|
||||
|
||||
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"
|
||||
);
|
||||
});
|
||||
|
||||
it("should hide the title", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
hide_title: true,
|
||||
});
|
||||
|
||||
expect(document.getElementsByClassName("header")[0]).toBeUndefined();
|
||||
expect(document.getElementsByTagName("svg")[0]).toHaveAttribute(
|
||||
"height",
|
||||
"165"
|
||||
);
|
||||
expect(queryByTestId(document.body, "card-body-content")).toHaveAttribute(
|
||||
"transform",
|
||||
"translate(0, -30)"
|
||||
);
|
||||
});
|
||||
|
||||
it("should not hide the title", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, {});
|
||||
|
||||
expect(document.getElementsByClassName("header")[0]).toBeDefined();
|
||||
expect(document.getElementsByTagName("svg")[0]).toHaveAttribute(
|
||||
"height",
|
||||
"195"
|
||||
);
|
||||
expect(queryByTestId(document.body, "card-body-content")).toHaveAttribute(
|
||||
"transform",
|
||||
"translate(0, 0)"
|
||||
);
|
||||
});
|
||||
|
||||
it("should render icons correctly", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
show_icons: true,
|
||||
});
|
||||
|
||||
expect(queryAllByTestId(document.body, "icon")[0]).toBeDefined();
|
||||
expect(queryByTestId(document.body, "stars")).toBeDefined();
|
||||
expect(
|
||||
queryByTestId(document.body, "stars").previousElementSibling // the label
|
||||
).toHaveAttribute("x", "25");
|
||||
});
|
||||
|
||||
it("should not have icons if show_icons is false", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, { show_icons: false });
|
||||
|
||||
console.log(queryAllByTestId(document.body, "icon"));
|
||||
expect(queryAllByTestId(document.body, "icon")[0]).not.toBeDefined();
|
||||
expect(queryByTestId(document.body, "stars")).toBeDefined();
|
||||
expect(
|
||||
queryByTestId(document.body, "stars").previousElementSibling // the label
|
||||
).not.toHaveAttribute("x");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
require("@testing-library/jest-dom");
|
||||
const retryer = require("../src/retryer");
|
||||
|
||||
const fetcher = jest.fn((variables, token) => {
|
||||
console.log(variables, token);
|
||||
return new Promise((res, rej) => res({ data: "ok" }));
|
||||
});
|
||||
|
||||
const fetcherFail = jest.fn(() => {
|
||||
return new Promise((res, rej) =>
|
||||
res({ data: { errors: [{ type: "RATE_LIMITED" }] } })
|
||||
);
|
||||
});
|
||||
|
||||
const fetcherFailOnSecondTry = jest.fn((_vars, _token, retries) => {
|
||||
return new Promise((res, rej) => {
|
||||
// faking rate limit
|
||||
if (retries < 1) {
|
||||
return res({ data: { errors: [{ type: "RATE_LIMITED" }] } });
|
||||
}
|
||||
return res({ data: "ok" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test Retryer", () => {
|
||||
it("retryer should return value and have zero retries on first try", async () => {
|
||||
let res = await retryer(fetcher, {});
|
||||
|
||||
expect(fetcher).toBeCalledTimes(1);
|
||||
expect(res).toStrictEqual({ data: "ok" });
|
||||
});
|
||||
|
||||
it("retryer should return value and have 2 retries", async () => {
|
||||
let res = await retryer(fetcherFailOnSecondTry, {});
|
||||
|
||||
expect(fetcherFailOnSecondTry).toBeCalledTimes(2);
|
||||
expect(res).toStrictEqual({ data: "ok" });
|
||||
});
|
||||
|
||||
it("retryer should throw error if maximum retries reached", async () => {
|
||||
let res;
|
||||
|
||||
try {
|
||||
res = await retryer(fetcherFail, {});
|
||||
} catch (err) {
|
||||
expect(fetcherFail).toBeCalledTimes(8);
|
||||
expect(err.message).toBe("Maximum retries exceeded");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
const { kFormatter, encodeHTML, renderError } = require("../src/utils");
|
||||
|
||||
describe("Test utils.js", () => {
|
||||
it("should test kFormatter", () => {
|
||||
expect(kFormatter(1)).toBe(1);
|
||||
expect(kFormatter(-1)).toBe(-1);
|
||||
expect(kFormatter(500)).toBe(500);
|
||||
expect(kFormatter(1000)).toBe("1k");
|
||||
expect(kFormatter(10000)).toBe("10k");
|
||||
expect(kFormatter(12345)).toBe("12.3k");
|
||||
expect(kFormatter(9900000)).toBe("9900k");
|
||||
});
|
||||
|
||||
it("should test encodeHTML", () => {
|
||||
expect(encodeHTML(`<html>hello world<,.#4^&^@%!))`)).toBe(
|
||||
"<html>hello world<,.#4^&^@%!))"
|
||||
);
|
||||
});
|
||||
|
||||
it("should test renderError", () => {
|
||||
document.body.innerHTML = renderError("Something went wrong");
|
||||
expect(document.getElementById("message").textContent).toBe(
|
||||
"Something went wrong"
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user