diff --git a/api/wild/index.js b/api/wild/index.js new file mode 100644 index 00000000..092ccdd1 --- /dev/null +++ b/api/wild/index.js @@ -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)); + } +}; diff --git a/api/wild/pin.js b/api/wild/pin.js new file mode 100644 index 00000000..cb5c244b --- /dev/null +++ b/api/wild/pin.js @@ -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)); + } +}; diff --git a/api/wild/top-langs.js b/api/wild/top-langs.js new file mode 100644 index 00000000..8fefa0e1 --- /dev/null +++ b/api/wild/top-langs.js @@ -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)); + } +}; diff --git a/readme.md b/readme.md index 29852bf9..d2fb0415 100644 --- a/readme.md +++ b/readme.md @@ -92,6 +92,7 @@ Visit 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 ``` +## 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): +

+ + + + +

+ +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/` + + + +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/` + + + +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/` + + + +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) diff --git a/src/cards/repo-card.js b/src/cards/repo-card.js index a306c1d5..1cf1de66 100644 --- a/src/cards/repo-card.js +++ b/src/cards/repo-card.js @@ -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} ${descriptionSvg} diff --git a/src/cards/stats-card.js b/src/cards/stats-card.js index f39a968f..5087921f 100644 --- a/src/cards/stats-card.js +++ b/src/cards/stats-card.js @@ -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(", "); diff --git a/src/cards/top-languages-card.js b/src/cards/top-languages-card.js index be1328c0..852341cc 100644 --- a/src/cards/top-languages-card.js +++ b/src/cards/top-languages-card.js @@ -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 ` ${name} - ${progress}% + ${text || `${progress}%`} ${createProgressNode({ x: 0, y: 25, @@ -89,7 +90,7 @@ const createCompactLangNode = ({ lang, totalSize, hideProgress, index }) => { - ${lang.name} ${hideProgress ? "" : percentage + "%"} + ${lang.name} ${hideProgress ? "" : (lang.text || `${percentage}%`)} `; @@ -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,