mirror of
https://github.com/Aviortheking/codestats-readme.git
synced 2025-07-05 21:09:19 +00:00
Merge branch 'master' into ranking-algo
This commit is contained in:
@ -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;
|
||||
|
||||
|
@ -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
97
src/getStyles.js
Normal 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;
|
@ -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)">
|
||||
|
@ -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
43
src/retryer.js
Normal 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;
|
28
src/utils.js
28
src/utils.js
@ -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,
|
||||
};
|
||||
|
Reference in New Issue
Block a user