Merge pull request #2527 from Zo-Bro-23/custom-cards

Transfer branch from my fork to main repo
This commit is contained in:
Zohan Subhash
2023-02-18 12:22:23 +05:30
committed by GitHub
7 changed files with 377 additions and 77 deletions
+75
View File
@@ -0,0 +1,75 @@
import { renderStatsCard } from "../../src/cards/stats-card.js";
import {
clampValue,
CONSTANTS,
parseArray,
parseBoolean,
renderError,
} from "../../src/common/utils.js";
export default async (req, res) => {
const {
hide,
hide_title,
hide_border,
card_width,
hide_rank,
show_icons,
line_height,
title_color,
ring_color,
icon_color,
text_color,
text_bold,
bg_color,
theme,
disable_animations,
border_radius,
border_color,
} = req.query;
res.setHeader("Content-Type", "image/svg+xml");
try {
const stats = {
totalStars: req.query.total_stars,
totalCommits: req.query.total_commits,
totalIssues: req.query.total_issues,
totalPRs: req.query.total_prs,
contributedTo: req.query.total_contribs,
rank: {
level: req.query.level || 'S',
score: req.query.score || 10
},
starsTitle: req.query.stars_title,
commitsTitle: req.query.commits_title,
issuesTitle: req.query.issues_title,
PRsTitle: req.query.prs_title,
contribsTitle: req.query.contribs_title,
title: req.query.title,
};
return res.send(
renderStatsCard(stats, {
hide: parseArray(hide),
show_icons: parseBoolean(show_icons),
hide_title: parseBoolean(hide_title),
hide_border: parseBoolean(hide_border),
card_width: parseInt(card_width, 10),
hide_rank: parseBoolean(hide_rank),
line_height,
title_color,
ring_color,
icon_color,
text_color,
text_bold: parseBoolean(text_bold),
bg_color,
theme,
border_radius,
border_color,
disable_animations: parseBoolean(disable_animations),
}),
);
} catch (err) {
return res.send(renderError(err.message, err.secondaryMessage));
}
};
+51
View File
@@ -0,0 +1,51 @@
import { renderRepoCard } from "../../src/cards/repo-card.js";
import {
clampValue,
CONSTANTS,
parseBoolean,
renderError,
} from "../../src/common/utils.js";
export default async (req, res) => {
const {
hide_border,
title_color,
icon_color,
text_color,
bg_color,
theme,
border_radius,
border_color,
} = req.query;
res.setHeader("Content-Type", "image/svg+xml");
try {
const repoData = {
name: req.query.title || "awesome/repo",
description: req.query.description || "What an awesome repo!",
primaryLanguage: {
name: req.query.footer || "Awesome",
color: req.query.badge ? `#${req.query.badge}` : "#4287f5",
},
starCount: req.query.stars || 12838,
forkCount: req.query.forks || 8929,
highlight: req.query.highlight || "AWESOME",
};
return res.send(
renderRepoCard(repoData, {
hide_border: parseBoolean(hide_border),
title_color,
icon_color,
text_color,
bg_color,
theme,
border_radius,
border_color,
}),
);
} catch (err) {
return res.send(renderError(err.message, err.secondaryMessage));
}
};
+84
View File
@@ -0,0 +1,84 @@
import { renderTopLanguages } from "../../src/cards/top-languages-card.js";
import {
clampValue,
CONSTANTS,
parseArray,
parseBoolean,
renderError,
} from "../../src/common/utils.js";
export default async (req, res) => {
const {
hide_title,
hide_border,
card_width,
title_color,
text_color,
bg_color,
theme,
layout,
border_radius,
border_color,
} = req.query;
res.setHeader("Content-Type", "image/svg+xml");
try {
let topLangs = []
if (req.query.langs) {
const split = req.query.langs.split(";");
split.forEach(lang => {
let {
name = "No Name",
size = "0",
color,
text,
} = JSON.parse(lang)
topLangs.push({
name,
size,
color: color ? `#${color}` : undefined,
text,
});
});
} else {
topLangs = [
{
name: "Javascript",
size: 50,
color: "#4287f5",
},
{
name: "Python",
size: 30,
color: "#eb4034",
},
{
name: "Ruby",
size: 20,
color: "#32a852",
}
];
}
const title = req.query.title || "Most Used Languages";
return res.send(
renderTopLanguages(topLangs, {
hide_title: parseBoolean(hide_title),
hide_border: parseBoolean(hide_border),
card_width: parseInt(card_width, 10),
title_color,
text_color,
bg_color,
theme,
layout,
langs_count: topLangs.length,
border_radius,
border_color,
title,
}),
);
} catch (err) {
return res.send(renderError(err.message, err.secondaryMessage));
}
};
+59
View File
@@ -92,6 +92,7 @@ Visit <https://indiafightscorona.giveindia.org> and make a small donation to hel
- [Repo Card Exclusive Options](#repo-card-exclusive-options)
- [Language Card Exclusive Options](#language-card-exclusive-options)
- [Wakatime Card Exclusive Option](#wakatime-card-exclusive-options)
- [Wild Cards](#wild-cards)
- [Deploy Yourself](#deploy-on-your-own-vercel-instance)
- [Keep your fork up to date](#keep-your-fork-up-to-date)
@@ -509,6 +510,64 @@ By default, GitHub does not lay out the cards side by side. To do that, you can
</a>
```
## Wild Cards
You can also generate custom cards with custom text and custom values (these do not show any GitHub statistics - they're custom values that you provide):
<p>
<img src="https://github-readme-stats.zohan.tech/api/wild/top-langs?langs=%7B%22name%22%3A%22Pop%22%2C%22size%22%3A60%2C%22color%22%3A%2232a852%22%2C%22text%22%3A%2260%20Hours%22%7D%3B%7B%22name%22%3A%22Reggae%22%2C%22size%22%3A40%2C%22color%22%3A%22fcba03%22%2C%22text%22%3A%2240%20Hours%22%7D%3B%7B%22name%22%3A%22Electronic%22%2C%22size%22%3A25%2C%22color%22%3A%224287f5%22%2C%22text%22%3A%2225%20Hours%22%7D%3B%7B%22name%22%3A%22Rap%22%2C%22size%22%3A15%2C%22color%22%3A%22eb4034%22%2C%22text%22%3A%2215%20Hours%22%7D%3B%7B%22name%22%3A%22Rock%22%2C%22size%22%3A10%2C%22color%22%3A%2232a852%22%2C%22text%22%3A%2210%20Hours%22%7D%3B%7B%22name%22%3A%22Other%22%2C%22size%22%3A5%2C%22color%22%3A%22fcba03%22%2C%22text%22%3A%225%20Hours%22%7D&title=Top%20Songs%20%F0%9F%8E%B5&theme=onedark&hide_border=true">
<img src="https://github-readme-stats.zohan.tech/api/wild?theme=onedark&level=CUST&score=50&title=Custom%20Cards%20%F0%9F%98%8E&show_icons=true&prs_title=Another%20Custom%20Title&total_stars=283%20Hours&stars_title=Non-numerical%20Value&commits_title=Recap&total_commits=%F0%9F%99%83&contribs_title=Big%20Number&total_contribs=12381&hide_border=true">
<img src="https://github-readme-stats.zohan.tech/api/wild/top-langs?langs=%7B%22name%22%3A%22Pop%22%2C%22size%22%3A60%2C%22color%22%3A%2232a852%22%7D%3B%7B%22name%22%3A%22Reggae%22%2C%22size%22%3A40%2C%22color%22%3A%22fcba03%22%7D%3B%7B%22name%22%3A%22Electronic%22%2C%22size%22%3A25%2C%22color%22%3A%224287f5%22%7D%3B%7B%22name%22%3A%22Rap%22%2C%22size%22%3A15%2C%22color%22%3A%22eb4034%22%7D%3B%7B%22name%22%3A%22Rock%22%2C%22size%22%3A10%2C%22color%22%3A%2232a852%22%7D%3B%7B%22name%22%3A%22Other%22%2C%22size%22%3A5%2C%22color%22%3A%22fcba03%22%7D&title=Top%20Songs%20%F0%9F%8E%B5&theme=onedark&layout=compact&hide_border=true">
<img src="https://github-readme-stats.zohan.tech/api/wild/pin?theme=onedark&title=Custom%20Repo%20%F0%9F%8E%86&description=Custom%20repo%20with%20a%20custom%20description!&highlight=CUSTOM&footer=Custom%20Language%20%F0%9F%93%9D&stars=The%20Best&hide_border=true">
</p>
The wild/custom cards can be generated by using the `/api/wild/`, `/api/wild/top-langs/`, and `/api/wild/pin/` endpoints. The following parameters can be provided:
### `/api/wild/`
<img src="https://github-readme-stats.zohan.tech/api/wild/?theme=dracula&hide_border=true&title=title&stars_title=stars_title&commits_title=commits_title&prs_title=prs_title&issues_title=issues_title&contribs_title=contribs_title&total_stars=total_stars&total_commits=total_commits&total_prs=total_prs&total_issues=total_issues&total_contribs=total_contribs&level=level&score=90">
Additionally, the `score` parameter must be given a value from `0` to `100` where `100` doesn't fill the circle at all, and `0` fills the circle completely (the demo above has `score=90`).
### `/api/wild/top-langs/`
<img src="https://github-readme-stats.zohan.tech/api/wild/top-langs?theme=dracula&hide_border=true&title=title&langs={%22name%22:%22Category1%22,%22size%22:30,%22color%22:%224287f5%22,%22text%22:%223%20thousand%22};{%22name%22:%22Category2%22,%22size%22:20,%22color%22:%22fcba03%22,%22text%22:%222%20thousand%22}">
Here, the languages are defined using the `langs` parameter. It is of the following format (JSON with `;` separating the objects):
```json
{
"name": "Category1",
"size": 60,
"color": "4287f5",
"text": "3 thousand"
};
{
"name": "Category2",
"size": 40,
"color": "fcba03",
"text": "3 thousand"
}
```
`name` is the name of the category. `size` is an arbitary size used to calculate percentages. This can be hours, percent, or any other unit you wish to use. `color` is the color of the category. `text` is what text to display next to the categry. If left blank, a percentage will be calculated and displayed.
### `/api/wild/pin/`
<img src="https://github-readme-stats.zohan.tech/api/wild/pin?theme=dracula&hide_border=true&title=title&description=description&highlight=highlight&stars=stars&forks=forks&footer=footer&badge=4287f5">
Here, the `badge` parameter was used to set the color of the small round icon (`4287f5`) was passed.
Finally, here are some specifications that apply to all three endpoints.
### All Three
Colors are passed as hex codes without the `#` character. For example, `ffffff`. This is the same as the main API.
Parameters like `total_stars` and `forks` can accept both numbers and text. If a number is given, it will automatically be formatted to use k-notation for thousands.
Parameters that apply only to the main API such as `username`, `repo`, `show_owner`, `locale`, etc won't work for this API. Other parameters like `layout` and `text_color` ***will*** work.
The new parameters that have been added are explained above (shown in the images). Parameters are lowercase snake-cased like for the main API.
Finally, no parameter is required, and parameters that aren't given will default to the most sensible option (original titles, ambigous descriptions, theme-based colors, and fake numbers).
## Deploy on your own Vercel instance
#### :film_projector: [Check Out Step By Step Video Tutorial By @codeSTACKr](https://youtu.be/n6d4KHSKqGk?t=107)
+51 -35
View File
@@ -87,16 +87,6 @@ const iconWithLabel = (icon, label, testid) => {
* @returns {string} Repository card SVG object.
*/
const renderRepoCard = (repo, options = {}) => {
const {
name,
nameWithOwner,
description,
primaryLanguage,
isArchived,
isTemplate,
starCount,
forkCount,
} = repo;
const {
hide_border = false,
title_color,
@@ -109,6 +99,42 @@ const renderRepoCard = (repo, options = {}) => {
border_color,
locale,
} = options;
const i18n = new I18n({
locale,
translations: repoCardLocales,
});
const colors = getCardColors({
title_color,
icon_color,
text_color,
bg_color,
border_color,
theme,
});
const {
name,
nameWithOwner,
description,
primaryLanguage,
isArchived,
isTemplate,
starCount,
forkCount,
} = repo;
const highlight = repo.highlight
? // @ts-ignore
getBadgeSVG(repo.highlight, colors.textColor)
: isTemplate
? // @ts-ignore
getBadgeSVG(i18n.t("repocard.template"), colors.textColor)
: isArchived
? // @ts-ignore
getBadgeSVG(i18n.t("repocard.archived"), colors.textColor)
: ""
const lineHeight = 10;
const header = show_owner ? nameWithOwner : name;
@@ -125,27 +151,25 @@ const renderRepoCard = (repo, options = {}) => {
const height =
(descriptionLines > 1 ? 120 : 110) + descriptionLines * lineHeight;
const i18n = new I18n({
locale,
translations: repoCardLocales,
});
// returns theme based colors with proper overrides and defaults
const colors = getCardColors({
title_color,
icon_color,
text_color,
bg_color,
border_color,
theme,
});
const svgLanguage = primaryLanguage
? createLanguageNode(langName, langColor)
: "";
const totalStars = kFormatter(starCount);
const totalForks = kFormatter(forkCount);
let totalStars
let totalForks
if (isNaN(starCount)) {
totalStars = starCount;
} else {
totalStars = kFormatter(starCount);
}
if (isNaN(forkCount)) {
totalForks = forkCount;
} else {
totalForks = kFormatter(forkCount);
}
const svgStars = iconWithLabel(icons.star, totalStars, "stargazers");
const svgForks = iconWithLabel(icons.fork, totalForks, "forkcount");
@@ -180,15 +204,7 @@ const renderRepoCard = (repo, options = {}) => {
`);
return card.render(`
${
isTemplate
? // @ts-ignore
getBadgeSVG(i18n.t("repocard.template"), colors.textColor)
: isArchived
? // @ts-ignore
getBadgeSVG(i18n.t("repocard.archived"), colors.textColor)
: ""
}
${highlight}
<text class="description" x="25" y="-5">
${descriptionSvg}
+44 -32
View File
@@ -40,7 +40,12 @@ const createTextNode = ({
shiftValuePos,
bold,
}) => {
const kValue = kFormatter(value);
let kValue
if (isNaN(value)) {
kValue = value;
} else {
kValue = kFormatter(value);
}
const staggerDelay = (index + 3) * 150;
const labelOffset = showIcons ? `x="25"` : "";
@@ -75,15 +80,6 @@ const createTextNode = ({
* @returns {string} The stats card SVG object.
*/
const renderStatsCard = (stats = {}, options = { hide: [] }) => {
const {
name,
totalStars,
totalCommits,
totalIssues,
totalPRs,
contributedTo,
rank,
} = stats;
const {
hide = [],
show_icons = false,
@@ -106,6 +102,37 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
locale,
disable_animations = false,
} = options;
const name = stats.name || "GitHub User"
const apostrophe = ["x", "s"].includes(name.slice(-1).toLocaleLowerCase())
? ""
: "s";
const i18n = new I18n({
locale,
translations: statCardLocales({ name, apostrophe }),
});
const {
totalStars = "2018",
totalCommits = "29019",
totalIssues = "203",
totalPRs = "127",
contributedTo = "232",
rank = {
level: "S",
score: 10
},
starsTitle = i18n.t("statcard.totalstars"),
commitsTitle = `${i18n.t("statcard.commits")}${
include_all_commits ? "" : ` (${new Date().getFullYear()})`
}`,
issuesTitle = i18n.t("statcard.issues"),
PRsTitle = i18n.t("statcard.prs"),
contribsTitle = i18n.t("statcard.contribs") + " (last year)",
title = i18n.t("statcard.title"),
} = stats;
const lheight = parseInt(String(line_height), 10);
@@ -121,45 +148,35 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
theme,
});
const apostrophe = ["x", "s"].includes(name.slice(-1).toLocaleLowerCase())
? ""
: "s";
const i18n = new I18n({
locale,
translations: statCardLocales({ name, apostrophe }),
});
// Meta data for creating text nodes with createTextNode function
const STATS = {
stars: {
icon: icons.star,
label: i18n.t("statcard.totalstars"),
label: starsTitle,
value: totalStars,
id: "stars",
},
commits: {
icon: icons.commits,
label: `${i18n.t("statcard.commits")}${
include_all_commits ? "" : ` (${new Date().getFullYear()})`
}`,
label: commitsTitle,
value: totalCommits,
id: "commits",
},
prs: {
icon: icons.prs,
label: i18n.t("statcard.prs"),
label: PRsTitle,
value: totalPRs,
id: "prs",
},
issues: {
icon: icons.issues,
label: i18n.t("statcard.issues"),
label: issuesTitle,
value: totalIssues,
id: "issues",
},
contribs: {
icon: icons.contribs,
label: i18n.t("statcard.contribs") + " (last year)",
label: contribsTitle,
value: contributedTo,
id: "contribs",
},
@@ -215,7 +232,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
});
const calculateTextWidth = () => {
return measureText(custom_title ? custom_title : i18n.t("statcard.title"));
return measureText(custom_title ? custom_title : title);
};
/*
@@ -241,7 +258,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
const card = new Card({
customTitle: custom_title,
defaultTitle: i18n.t("statcard.title"),
defaultTitle: title,
width,
height,
border_radius,
@@ -306,11 +323,6 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
const labels = Object.keys(STATS)
.filter((key) => !hide.includes(key))
.map((key) => {
if (key === "commits") {
return `${i18n.t("statcard.commits")} ${
include_all_commits ? "" : `in ${new Date().getFullYear()}`
} : ${totalStars}`;
}
return `${STATS[key].label}: ${STATS[key].value}`;
})
.join(", ");
+13 -10
View File
@@ -47,6 +47,7 @@ const getLongestLang = (arr) =>
* @param {number} props.index Index of the programming language.
* @returns {string} Programming language SVG node.
*/
const createProgressTextNode = ({ width, color, name, progress, index }) => {
const staggerDelay = (index + 3) * 150;
const paddingRight = 95;
@@ -56,7 +57,7 @@ const createProgressTextNode = ({ width, color, name, progress, index }) => {
return `
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
<text data-testid="lang-name" x="2" y="15" class="lang-name">${name}</text>
<text x="${progressTextX}" y="34" class="lang-name">${progress}%</text>
<text x="${progressTextX}" y="34" class="lang-name">${text || `${progress}%`}</text>
${createProgressNode({
x: 0,
y: 25,
@@ -89,7 +90,7 @@ const createCompactLangNode = ({ lang, totalSize, hideProgress, index }) => {
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
<circle cx="5" cy="6" r="5" fill="${color}" />
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
${lang.name} ${hideProgress ? "" : percentage + "%"}
${lang.name} ${hideProgress ? "" : (lang.text || `${percentage}%`)}
</text>
</g>
`;
@@ -126,7 +127,7 @@ const createLanguageTextNode = ({ langs, totalSize, hideProgress }) => {
const percent = ((longestLang.size / totalSize) * 100).toFixed(2);
const minGap = 150;
const maxGap = 20 + measureText(`${longestLang.name} ${percent}%`, 11);
const maxGap = 20 + measureText(`${longestLang.name} ${longestLang.text || `${percent}%`}`, 11);
return flexLayout({
items: layouts,
gap: maxGap < minGap ? minGap : maxGap,
@@ -149,6 +150,7 @@ const renderNormalLayout = (langs, width, totalLanguageSize) => {
name: lang.name,
color: lang.color || DEFAULT_LANG_COLOR,
progress: ((lang.size / totalLanguageSize) * 100).toFixed(2),
text: lang.text,
index,
});
}),
@@ -278,6 +280,12 @@ const useLanguages = (topLangs, hide, langs_count) => {
* @returns {string} Language card SVG object.
*/
const renderTopLanguages = (topLangs, options = {}) => {
const locale = options.locale
const i18n = new I18n({
locale,
translations: langCardLocales,
});
const {
hide_title = false,
hide_border,
@@ -290,18 +298,13 @@ const renderTopLanguages = (topLangs, options = {}) => {
theme,
layout,
custom_title,
locale,
langs_count = DEFAULT_LANGS_COUNT,
border_radius,
border_color,
title = i18n.t("langcard.title"),
disable_animations,
} = options;
const i18n = new I18n({
locale,
translations: langCardLocales,
});
const { langs, totalLanguageSize } = useLanguages(
topLangs,
hide,
@@ -342,7 +345,7 @@ const renderTopLanguages = (topLangs, options = {}) => {
const card = new Card({
customTitle: custom_title,
defaultTitle: i18n.t("langcard.title"),
defaultTitle: title,
width,
height,
border_radius,