Merge branch 'master' into ranking-algo

This commit is contained in:
anuraghazra
2020-07-15 19:47:07 +05:30
13 changed files with 347 additions and 107 deletions

View File

@ -1,12 +1,10 @@
const { request } = require("./utils");
const retryer = require("./retryer");
async function fetchRepo(username, reponame) {
if (!username || !reponame) {
throw new Error("Invalid username or reponame");
}
const res = await request({
query: `
const fetcher = (variables, token) => {
return request(
{
query: `
fragment RepoInfo on Repository {
name
stargazers {
@ -33,11 +31,20 @@ async function fetchRepo(username, reponame) {
}
}
`,
variables: {
login: username,
repo: reponame,
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;

View File

@ -1,26 +1,26 @@
const { request } = require("./utils");
const retryer = require("./retryer");
const calculateRank = require("./calculateRank");
require("dotenv").config();
async function fetchStats(username) {
if (!username) throw Error("Invalid username");
const res = await request({
query: `
const fetcher = (variables, token) => {
return 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) {
repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
totalCount
}
issues(first: 100) {
pullRequests(first: 1) {
totalCount
}
issues(first: 1) {
totalCount
}
followers {
@ -36,9 +36,17 @@ async function fetchStats(username) {
}
}
}
`,
variables: { login: username },
});
`,
variables,
},
{
Authorization: `bearer ${token}`,
}
);
};
async function fetchStats(username) {
if (!username) throw Error("Invalid username");
const stats = {
name: "",
@ -47,12 +55,14 @@ async function fetchStats(username) {
totalIssues: 0,
totalStars: 0,
contributedTo: 0,
rank: "C",
rank: { level: "C", score: 0 },
};
let res = await retryer(fetcher, { login: username });
if (res.data.errors) {
console.log(res.data.errors);
throw Error("Could not fetch user");
throw Error(res.data.errors[0].message || "Could not fetch user");
}
const user = res.data.data.user;

97
src/getStyles.js Normal file
View File

@ -0,0 +1,97 @@
const calculateCircleProgress = (value) => {
let radius = 40;
let c = Math.PI * (radius * 2);
if (value < 0) value = 0;
if (value > 100) value = 100;
let percentage = ((100 - value) / 100) * c;
return percentage;
};
const getAnimations = ({ progress }) => {
return `
/* Animations */
@keyframes scaleIn {
from {
transform: translate(-5px, 5px) scale(0);
}
to {
transform: translate(-5px, 5px) scale(1);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes rankAnimation {
from {
stroke-dashoffset: ${calculateCircleProgress(0)};
}
to {
stroke-dashoffset: ${calculateCircleProgress(progress)};
}
}
`;
};
const getStyles = ({
titleColor,
textColor,
iconColor,
show_icons,
progress,
}) => {
return `
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor};
animation: fadeIn 0.8s ease-in-out forwards;
}
.stat {
font: 600 14px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor};
}
.stagger {
opacity: 0;
animation: fadeIn 0.3s ease-in-out forwards;
}
.rank-text {
font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor};
animation: scaleIn 0.3s ease-in-out forwards;
}
.bold { font-weight: 700 }
.star-icon {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
}
.icon {
fill: ${iconColor};
display: ${!!show_icons ? "block" : "none"};
}
.rank-circle-rim {
stroke: ${titleColor};
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: ${titleColor};
stroke-dasharray: 250;
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
${process.env.NODE_ENV === "test" ? "" : getAnimations({ progress })}
`;
};
module.exports = getStyles;

View File

@ -1,23 +1,24 @@
const { kFormatter, encodeHTML, isValidHexColor } = require("../src/utils");
const { kFormatter, encodeHTML, fallbackColor } = require("../src/utils");
const renderRepoCard = (repo, options = {}) => {
const { name, description, primaryLanguage, stargazers, forkCount } = repo;
const { title_color, icon_color, text_color, bg_color } = options;
const langName = primaryLanguage ? primaryLanguage.name : "Unspecified";
const langColor = primaryLanguage ? primaryLanguage.color : "#333";
const height = 120;
const shiftText = primaryLanguage.name.length > 15 ? 0 : 30;
const shiftText = langName.length > 15 ? 0 : 30;
let desc = description || "No description provided";
if (desc.length > 55) {
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 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);
@ -38,12 +39,8 @@ const renderRepoCard = (repo, options = {}) => {
<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="${
primaryLanguage.color
}" />
<text data-testid="lang" class="gray" x="15">${
primaryLanguage.name
}</text>
<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)">

View File

@ -1,12 +1,15 @@
const { kFormatter, isValidHexColor } = require("../src/utils");
const { kFormatter, fallbackColor } = require("../src/utils");
const getStyles = require("./getStyles");
const createTextNode = ({ icon, label, value, id, index, lineHeight }) => {
const classname = icon === "★" && "star-icon";
const kValue = kFormatter(value);
const staggerDelay = (index + 3) * 150;
// manually calculating lineHeight based on index instead of using <tspan dy="" />
// to fix firefox layout bug
const lheight = lineHeight * (index + 1);
return `
<text x="25" y="${lineHeight * (index + 1)}">
<text class="stagger" style="animation-delay: ${staggerDelay}ms" x="25" y="${lheight}">
<tspan dx="0" data-testid="icon" class="icon ${classname}">${icon}</tspan>
<tspan dx="0" class="stat bold">
${label}:
@ -40,12 +43,10 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
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 titleColor = fallbackColor(title_color, "#2f80ed");
const iconColor = fallbackColor(icon_color, "#4c71f2");
const textColor = fallbackColor(text_color, "#333");
const bgColor = fallbackColor(bg_color, "#FFFEFE");
const STATS = {
stars: {
@ -107,7 +108,6 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
/>
`;
const rankProgress = 180 + rank.score * 0.8;
const rankCircle = hide_rank
? ""
: `<g data-testid="rank-circle" transform="translate(400, ${
@ -116,48 +116,40 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
<circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
<circle class="rank-circle" cx="-10" cy="8" r="40" />
<text
x="0"
x="${rank.level.length === 1 ? "-4" : "0"}"
y="0"
alignment-baseline="central"
dominant-baseline="central"
text-anchor="middle"
class="rank-text"
transform="translate(-5, 5)"
>
${rank.level}
</text>
</g>`;
// re-adjust circle progressbar's value until the ranking algo is improved
let progress = rank.score;
if (rank.score > 86) {
progress = (40 + rank.score) * 0.6;
}
if (rank.score < 40) {
progress = 40 + rank.score;
}
const styles = getStyles({
titleColor,
textColor,
iconColor,
show_icons,
progress,
});
return `
<svg width="495" height="${height}" viewBox="0 0 495 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
.header { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor}; }
.stat { font: 600 14px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor}; }
.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-rim {
stroke: ${titleColor};
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.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);
}
${styles}
</style>
${hide_border ? "" : border}
${rankCircle}

43
src/retryer.js Normal file
View File

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

View File

@ -33,13 +33,27 @@ function isValidHexColor(hexColor) {
).test(hexColor);
}
function request(data) {
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 new Promise((resolve, reject) => {
axios({
url: "https://api.github.com/graphql",
method: "post",
headers: {
Authorization: `bearer ${process.env.GITHUB_TOKEN}`,
...headers,
},
data,
})
@ -48,4 +62,12 @@ function request(data) {
});
}
module.exports = { renderError, kFormatter, encodeHTML, isValidHexColor, request };
module.exports = {
renderError,
kFormatter,
encodeHTML,
isValidHexColor,
request,
parseBoolean,
fallbackColor,
};