Compare commits

...
Author SHA1 Message Date
anuraghazra ff292f2c0b docs: updated docs
Test / build (push) Canceled after 0s
2020-07-31 13:25:27 +05:30
anuraghazra 68e7cbe8b1 chore: added reference/links comments 2020-07-31 13:17:04 +05:30
anuraghazra 54a106ce36 chore: increased value offset x 2020-07-31 13:13:10 +05:30
Anurag Hazra 893c5e4df3 Merge branch 'master' into fix-total-commits 2020-07-31 13:07:37 +05:30
kingthorin a4486d0327 docs: typo fix (#269) 2020-07-31 12:59:25 +05:30
Anurag Hazra 60511778ff docs: removed customization table and simplified docs (#267) 2020-07-30 20:26:13 +05:30
Anurag Hazra 3b0f1b11a0 refactor: added reusable Card class to reduce code & test duplication (#260)
* refactor: added reusable Card class to reduce code & test duplication

* fix: top-langs card width & documented card_width option
2020-07-30 19:19:03 +05:30
Anurag Hazra 34b5dcb181 Merge pull request #255 from anuraghazra/xss-fix
fix: xss vulnerability on renderError
2020-07-29 19:06:57 +05:30
anuraghazra 0833e8531d fix: xss vulnerability on renderError 2020-07-29 19:02:06 +05:30
Anurag Hazra 3c7220e4f7 Merge pull request #253 from anuraghazra/badges
chore: added @ahmadawais support badges
2020-07-29 13:08:06 +05:30
anuraghazra 03e484b1fd docs: added docs for include_all_commits 2020-07-27 20:27:07 +05:30
anuraghazra 9753c24625 chore: remove console logs 2020-07-27 20:18:29 +05:30
anuraghazra 331721945f tests: added tests 2020-07-27 20:12:14 +05:30
anuraghazra 56d424f1d4 fix: wip fix total commit counts 2020-07-27 14:45:49 +05:30
15 changed files with 575 additions and 255 deletions
+7 -1
View File
@@ -18,6 +18,7 @@ module.exports = async (req, res) => {
hide_rank,
show_icons,
count_private,
include_all_commits,
line_height,
title_color,
icon_color,
@@ -31,7 +32,11 @@ module.exports = async (req, res) => {
res.setHeader("Content-Type", "image/svg+xml");
try {
stats = await fetchStats(username, parseBoolean(count_private));
stats = await fetchStats(
username,
parseBoolean(count_private),
parseBoolean(include_all_commits)
);
} catch (err) {
return res.send(
renderError(
@@ -56,6 +61,7 @@ module.exports = async (req, res) => {
hide_title: parseBoolean(hide_title),
hide_border: parseBoolean(hide_border),
hide_rank: parseBoolean(hide_rank),
include_all_commits: parseBoolean(include_all_commits),
line_height,
title_color,
icon_color,
+4 -3
View File
@@ -14,13 +14,14 @@ module.exports = async (req, res) => {
username,
hide,
hide_title,
hide_border,
card_width,
title_color,
text_color,
bg_color,
theme,
cache_seconds,
layout
layout,
} = req.query;
let topLangs;
@@ -42,15 +43,15 @@ module.exports = async (req, res) => {
res.send(
renderTopLanguages(topLangs, {
theme,
hide_title: parseBoolean(hide_title),
hide_border: parseBoolean(hide_border),
card_width: parseInt(card_width, 10),
hide: parseArray(hide),
title_color,
text_color,
bg_color,
theme,
layout
layout,
})
);
};
+1
View File
@@ -22,6 +22,7 @@
"dependencies": {
"dotenv": "^8.2.0",
"emoji-name-map": "^1.2.8",
"github-username-regex": "^1.0.0",
"word-wrap": "^1.2.3"
},
"husky": {
+38 -22
View File
@@ -118,28 +118,44 @@ You can look at a preview for [all available themes](./themes/README.md) or chec
You can customize the appearance of your `Stats Card` or `Repo Card` however you want with URL params.
Customization Options:
#### Common Options:
| Option | type | description | Stats Card (default) | Repo Card (default) | Top Lang Card (default) |
| ------------- | --------- | ------------------------------------------- | -------------------- | ------------------- | ----------------------- |
| title_color | hex color | title color | 2f80ed | 2f80ed | 2f80ed |
| text_color | hex color | body color | 333 | 333 | 333 |
| icon_color | hex color | icon color | 4c71f2 | 586069 | 586069 |
| bg_color | hex color | card bg color | FFFEFE | FFFEFE | FFFEFE |
| line_height | number | control the line-height between text | 30 | N/A | N/A |
| hide | CSV | hides the items specified | undefined | N/A | undefined |
| hide_rank | boolean | hides the ranking | false | N/A | N/A |
| hide_title | boolean | hides the stats title | false | N/A | false |
| hide_border | boolean | hides the stats card border | false | N/A | N/A |
| show_owner | boolean | shows owner name in repo card | N/A | false | N/A |
| show_icons | boolean | shows icons | false | N/A | N/A |
| theme | string | sets inbuilt theme | 'default' | 'default_repocard' | 'default' |
| cache_seconds | number | manually set custom cache control | 1800 | 1800 | 1800 |
| count_private | boolean | counts private contributions too if enabled | false | N/A | N/A |
| layout | string | choose a layout option | N/A | N/A | 'default' |
- `title_color` - Card's title color _(hex color)_
- `text_color` - Body text color _(hex color)_
- `icon_color` - Icons color if available _(hex color)_
- `bg_color` - Card's background color _(hex color)_
- `theme` - name of the theme, choose from [all available themes](./themes/README.md)
- `cache_seconds` - set the cache header manually _(min: 1800, max: 86400)_
> Note on cache: Repo cards have default cache of 30mins (1800 seconds) if the fork count & star count is less than 1k otherwise it's 2hours (7200). Also note that cache is clamped to minimum of 30min and maximum of 24hours
#### Stats Card Exclusive Options:
- `hide` - Hide's the specified items from stats _(Comma seperated values)_
- `hide_title` - _(boolean)_
- `hide_rank` - _(boolean)_
- `show_icons` - _(boolean)_
- `include_total_commits` - Count total commits instead of just the current year commits _(boolean)_
- `count_private` - Count private commits _(boolean)_
- `line_height` - Sets the line-height between text _(number)_
#### Repo Card Exclusive Options:
- `show_owner` - Show the owner name of the repo _(boolean)_
#### Language Card Exclusive Options:
- `hide` - Hide the languages specified from the card _(Comma seperated values)_
- `hide_title` - _(boolean)_
- `layout` - Switch between two available layouts `default` & `compact`
- `card_width` - Set the card's width manually _(number)_
> :warning: **Important:**
> Language names should be uri-escaped, as specified in [Percent Encoding](https://en.wikipedia.org/wiki/Percent-encoding)
> (i.e: `c++` should become `c%2B%2B`, `jupyter notebook` should become `jupyter%20notebook`, etc.)
---
# GitHub Extra Pins
GitHub extra pins allow you to pin more than 6 repositories in your profile using a GitHub readme profile.
@@ -188,10 +204,6 @@ You can use `?hide=language1,language2` parameter to hide individual languages.
[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&hide=javascript,html)](https://github.com/anuraghazra/github-readme-stats)
```
> :warning: **Important:**
> Language names should be uri-escaped, as specified in [Percent Encoding](https://en.wikipedia.org/wiki/Percent-encoding)
> (i.e: `c++` should become `c%2B%2B`, `jupyter notebook` should become `jupyter%20notebook`, etc.)
### Compact Language Card Layout
You can use the `&layout=compact` option to change the card design.
@@ -224,6 +236,10 @@ You can use the `&layout=compact` option to change the card design.
![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=issues&show_icons=true)
- Include All Commits
![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&include_all_commits=true)
- Themes
Choose from any of the [default themes](#themes)
+139
View File
@@ -0,0 +1,139 @@
const { FlexLayout } = require("./utils");
const { getAnimations } = require("./getStyles");
class Card {
constructor({
width = 100,
height = 100,
colors = {},
title = "",
titlePrefixIcon,
}) {
this.width = width;
this.height = height;
this.hideBorder = false;
this.hideTitle = false;
// returns theme based colors with proper overrides and defaults
this.colors = colors;
this.title = title;
this.css = "";
this.paddingX = 25;
this.paddingY = 35;
this.titlePrefixIcon = titlePrefixIcon;
this.animations = true;
}
disableAnimations() {
this.animations = false;
}
setCSS(value) {
this.css = value;
}
setHideBorder(value) {
this.hideBorder = value;
}
setHideTitle(value) {
this.hideTitle = value;
if (value) {
this.height -= 30;
}
}
setTitle(text) {
this.title = text;
}
renderTitle() {
const titleText = `
<text
x="0"
y="0"
class="header"
data-testid="header"
>${this.title}</text>
`;
const prefixIcon = `
<svg
class="icon"
x="0"
y="-13"
viewBox="0 0 16 16"
version="1.1"
width="16"
height="16"
>
${this.titlePrefixIcon}
</svg>
`;
return `
<g
data-testid="card-title"
transform="translate(${this.paddingX}, ${this.paddingY})"
>
${FlexLayout({
items: [this.titlePrefixIcon && prefixIcon, titleText],
gap: 25,
}).join("")}
</g>
`;
}
render(body) {
return `
<svg
width="${this.width}"
height="${this.height}"
viewBox="0 0 ${this.width} ${this.height}"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: ${this.colors.titleColor};
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
${this.css}
${
process.env.NODE_ENV === "test" || !this.animations
? ""
: getAnimations()
}
</style>
<rect
data-testid="card-bg"
x="0.5"
y="0.5"
rx="4.5"
height="99%"
stroke="#E4E2E2"
width="${this.width - 1}"
fill="${this.colors.bgColor}"
stroke-opacity="${this.hideBorder ? 0 : 1}"
/>
${this.hideTitle ? "" : this.renderTitle()}
<g
data-testid="main-card-body"
transform="translate(0, ${
this.hideTitle ? this.paddingX : this.paddingY + 20
})"
>
${body}
</g>
</svg>
`;
}
}
module.exports = Card;
+51 -5
View File
@@ -1,6 +1,9 @@
const { request, logger } = require("./utils");
const axios = require("axios");
const retryer = require("./retryer");
const calculateRank = require("./calculateRank");
const githubUsernameRegex = require("github-username-regex");
require("dotenv").config();
const fetcher = (variables, token) => {
@@ -46,7 +49,45 @@ const fetcher = (variables, token) => {
);
};
async function fetchStats(username, count_private = false) {
// https://github.com/anuraghazra/github-readme-stats/issues/92#issuecomment-661026467
// https://github.com/anuraghazra/github-readme-stats/pull/211/
const totalCommitsFetcher = async (username) => {
if (!githubUsernameRegex.test(username)) {
logger.log("Invalid username");
return 0;
}
// https://developer.github.com/v3/search/#search-commits
const fetchTotalCommits = (variables, token) => {
return axios({
method: "get",
url: `https://api.github.com/search/commits?q=author:${variables.login}`,
headers: {
"Content-Type": "application/json",
Accept: "application/vnd.github.cloak-preview",
Authorization: `bearer ${token}`,
},
});
};
try {
let res = await retryer(fetchTotalCommits, { login: username });
if (res.data.total_count) {
return res.data.total_count;
}
} catch (err) {
logger.log(err);
// just return 0 if there is something wrong so that
// we don't break the whole app
return 0;
}
};
async function fetchStats(
username,
count_private = false,
include_all_commits = false
) {
if (!username) throw Error("Invalid username");
const stats = {
@@ -61,6 +102,11 @@ async function fetchStats(username, count_private = false) {
let res = await retryer(fetcher, { login: username });
let experimental_totalCommits = 0;
if (include_all_commits) {
experimental_totalCommits = await totalCommitsFetcher(username);
}
if (res.data.errors) {
logger.error(res.data.errors);
throw Error(res.data.errors[0].message || "Could not fetch user");
@@ -72,11 +118,11 @@ async function fetchStats(username, count_private = false) {
stats.name = user.name || user.login;
stats.totalIssues = user.issues.totalCount;
stats.totalCommits = contributionCount.totalCommitContributions;
stats.totalCommits =
contributionCount.totalCommitContributions + experimental_totalCommits;
if (count_private) {
stats.totalCommits =
contributionCount.totalCommitContributions +
contributionCount.restrictedContributionsCount;
stats.totalCommits += contributionCount.restrictedContributionsCount;
}
stats.totalPRs = user.pullRequests.totalCount;
+23 -23
View File
@@ -9,10 +9,23 @@ const calculateCircleProgress = (value) => {
return percentage;
};
const getAnimations = ({ progress }) => {
const getProgressAnimation = ({ progress }) => {
return `
@keyframes rankAnimation {
from {
stroke-dashoffset: ${calculateCircleProgress(0)};
}
to {
stroke-dashoffset: ${calculateCircleProgress(progress)};
}
}
`;
};
const getAnimations = () => {
return `
/* Animations */
@keyframes scaleIn {
@keyframes scaleInAnimation {
from {
transform: translate(-5px, 5px) scale(0);
}
@@ -20,7 +33,7 @@ const getAnimations = ({ progress }) => {
transform: translate(-5px, 5px) scale(1);
}
}
@keyframes fadeIn {
@keyframes fadeInAnimation {
from {
opacity: 0;
}
@@ -28,14 +41,6 @@ const getAnimations = ({ progress }) => {
opacity: 1;
}
}
@keyframes rankAnimation {
from {
stroke-dashoffset: ${calculateCircleProgress(0)};
}
to {
stroke-dashoffset: ${calculateCircleProgress(progress)};
}
}
`;
};
@@ -47,20 +52,16 @@ const getStyles = ({
progress,
}) => {
return `
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor};
animation: fadeIn 0.8s ease-in-out forwards;
}
.stat {
.stat {
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
}
.stagger {
.stagger {
opacity: 0;
animation: fadeIn 0.3s ease-in-out forwards;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
.rank-text {
.rank-text {
font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor};
animation: scaleIn 0.3s ease-in-out forwards;
animation: scaleInAnimation 0.3s ease-in-out forwards;
}
.bold { font-weight: 700 }
@@ -86,9 +87,8 @@ const getStyles = ({
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
${process.env.NODE_ENV === "test" ? "" : getAnimations({ progress })}
${process.env.NODE_ENV === "test" ? "" : getProgressAnimation({ progress })}
`;
};
module.exports = getStyles;
module.exports = { getStyles, getAnimations };
+59 -51
View File
@@ -7,6 +7,7 @@ const {
} = require("../src/utils");
const icons = require("./icons");
const toEmoji = require("emoji-name-map");
const Card = require("./Card");
const renderRepoCard = (repo, options = {}) => {
const {
@@ -84,68 +85,75 @@ const renderRepoCard = (repo, options = {}) => {
`
: "";
const iconWithLabel = (icon, label, testid) => {
return `
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
${icon}
</svg>
<text data-testid="${testid}" class="gray" x="25">${label}</text>
`;
};
const svgStars =
stargazers.totalCount > 0 &&
`
<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>
`;
iconWithLabel(icons.star, totalStars, "stargazers");
const svgForks =
forkCount > 0 &&
`
<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>
`;
forkCount > 0 && iconWithLabel(icons.fork, totalForks, "forkcount");
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} }
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
.badge rect { opacity: 0.2 }
</style>
const starAndForkCount = FlexLayout({
items: [svgStars, svgForks],
gap: 65,
}).join("");
<rect data-testid="card-bg" 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>
const card = new Card({
title: header,
titlePrefixIcon: icons.contribs,
width: 400,
height,
colors: {
titleColor,
textColor,
iconColor,
bgColor,
},
});
<text x="50" y="38" class="header">${header}</text>
card.disableAnimations();
card.setHideBorder(false);
card.setHideTitle(false);
card.setCSS(`
.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} }
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
.badge rect { opacity: 0.2 }
`);
${
isTemplate
? getBadgeSVG("Template")
: isArchived
? getBadgeSVG("Archived")
: ""
}
return card.render(`
${
isTemplate
? getBadgeSVG("Template")
: isArchived
? getBadgeSVG("Archived")
: ""
}
<text class="description" x="25" y="50">
${multiLineDescription
.map((line) => `<tspan dy="1.2em" x="25">${encodeHTML(line)}</tspan>`)
.join("")}
</text>
<text class="description" x="25" y="-5">
${multiLineDescription
.map((line) => `<tspan dy="1.2em" x="25">${encodeHTML(line)}</tspan>`)
.join("")}
</text>
<g transform="translate(0, ${height - 20})">
${svgLanguage}
<g transform="translate(0, ${height - 75})">
${svgLanguage}
<g
data-testid="star-fork-group"
transform="translate(${primaryLanguage ? 155 - shiftText : 25}, 0)"
>
${FlexLayout({ items: [svgStars, svgForks], gap: 65 }).join("")}
</g>
<g
data-testid="star-fork-group"
transform="translate(${primaryLanguage ? 155 - shiftText : 25}, 0)"
>
${starAndForkCount}
</g>
</svg>
`;
</g>
`);
};
module.exports = renderRepoCard;
+61 -65
View File
@@ -4,10 +4,19 @@ const {
FlexLayout,
encodeHTML,
} = require("../src/utils");
const getStyles = require("./getStyles");
const { getStyles } = require("./getStyles");
const icons = require("./icons");
const Card = require("./Card");
const createTextNode = ({ icon, label, value, id, index, showIcons }) => {
const createTextNode = ({
icon,
label,
value,
id,
index,
showIcons,
shiftValuePos,
}) => {
const kValue = kFormatter(value);
const staggerDelay = (index + 3) * 150;
@@ -23,7 +32,12 @@ const createTextNode = ({ icon, label, value, id, index, showIcons }) => {
<g class="stagger" style="animation-delay: ${staggerDelay}ms" transform="translate(25, 0)">
${iconSvg}
<text class="stat bold" ${labelOffset} y="12.5">${label}:</text>
<text class="stat" x="135" y="12.5" data-testid="${id}">${kValue}</text>
<text
class="stat"
x="${shiftValuePos ? 170 : 150}"
y="12.5"
data-testid="${id}"
>${kValue}</text>
</g>
`;
};
@@ -44,6 +58,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
hide_title = false,
hide_border = false,
hide_rank = false,
include_all_commits = false,
line_height = 25,
title_color,
icon_color,
@@ -52,7 +67,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
theme = "default",
} = options;
const lheight = parseInt(line_height);
const lheight = parseInt(line_height, 10);
// returns theme based colors with proper overrides and defaults
const { titleColor, textColor, iconColor, bgColor } = getCardColors({
@@ -73,7 +88,9 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
},
commits: {
icon: icons.commits,
label: "Total Commits",
label: `Total Commits${
include_all_commits ? "" : ` (${new Date().getFullYear()})`
}`,
value: totalCommits,
id: "commits",
},
@@ -106,6 +123,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
...STATS[key],
index,
showIcons: show_icons,
shiftValuePos: !include_all_commits,
})
);
@@ -116,44 +134,11 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
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 apostrophe = ["x", "s"].includes(name.slice(-1)) ? "" : "s";
const title = hide_title
? ""
: `<text x="25" y="35" class="header">${encodeHTML(name)}'${apostrophe} GitHub Stats</text>`;
const border = `
<rect
data-testid="card-bg"
x="0.5"
y="0.5"
width="494"
height="99%"
rx="4.5"
fill="${bgColor}"
stroke="#E4E2E2"
stroke-opacity="${hide_border ? 0 : 1}"
/>
`;
const rankCircle = hide_rank
? ""
: `<g data-testid="rank-circle" transform="translate(400, ${
height / 1.85
})">
: `<g data-testid="rank-circle"
transform="translate(400, ${height / 2 - 50})">
<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">
@@ -169,34 +154,45 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
</g>
</g>`;
if (hide_title) {
height -= 30;
}
// 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 cssStyles = 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>
${styles}
</style>
${border}
${title}
const apostrophe = ["x", "s"].includes(name.slice(-1)) ? "" : "s";
const card = new Card({
title: `${encodeHTML(name)}'${apostrophe} GitHub Stats`,
width: 495,
height,
colors: {
titleColor,
textColor,
iconColor,
bgColor,
},
});
<g data-testid="card-body-content" transform="translate(0, ${
hide_title ? -30 : 0
})">
${rankCircle}
card.setHideBorder(hide_border);
card.setHideTitle(hide_title);
card.setCSS(cssStyles);
<svg x="0" y="55">
${FlexLayout({
items: statItems,
gap: lheight,
direction: "column",
}).join("")}
</svg>
</g>
</svg>
`;
return card.render(`
${rankCircle}
<svg x="0" y="0">
${FlexLayout({
items: statItems,
gap: lheight,
direction: "column",
}).join("")}
</svg>
`);
};
module.exports = renderStatsCard;
+22 -20
View File
@@ -1,4 +1,5 @@
const { getCardColors, FlexLayout, clampValue } = require("../src/utils");
const Card = require("./Card");
const createProgressNode = ({ width, color, name, progress }) => {
const paddingRight = 95;
@@ -63,6 +64,7 @@ const lowercaseTrim = (name) => name.toLowerCase().trim();
const renderTopLanguages = (topLangs, options = {}) => {
const {
hide_title,
hide_border,
card_width,
title_color,
text_color,
@@ -170,29 +172,29 @@ const renderTopLanguages = (topLangs, options = {}) => {
}).join("");
}
if (hide_title) {
height -= 30;
}
const card = new Card({
title: "Most Used Languages",
width,
height,
colors: {
titleColor,
textColor,
bgColor,
},
});
return `
<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
.header { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor} }
.lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
</style>
<rect data-testid="card-bg" x="0.5" y="0.5" width="99.7%" height="99%" rx="4.5" fill="${bgColor}" stroke="#E4E2E2"/>
card.disableAnimations();
card.setHideBorder(hide_border);
card.setHideTitle(hide_title);
card.setCSS(`
.lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
`);
${
hide_title
? ""
: `<text data-testid="header" x="25" y="35" class="header">Most Used Languages</text>`
}
<svg data-testid="lang-items" x="25" y="${hide_title ? 25 : 55}">
${finalLayout}
</svg>
return card.render(`
<svg data-testid="lang-items" x="25">
${finalLayout}
</svg>
`;
`);
};
module.exports = renderTopLanguages;
+1 -1
View File
@@ -13,7 +13,7 @@ const renderError = (message, secondaryMessage = "") => {
<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 data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">${message}</tspan>
<tspan x="25" dy="18">${encodeHTML(message)}</tspan>
<tspan x="25" dy="18" class="gray">${secondaryMessage}</tspan>
</text>
</svg>
+136
View File
@@ -0,0 +1,136 @@
require("@testing-library/jest-dom");
const cssToObject = require("css-to-object");
const Card = require("../src/Card");
const icons = require("../src/icons");
const { getCardColors } = require("../src/utils");
const { queryByTestId } = require("@testing-library/dom");
describe("Card", () => {
it("should hide border", () => {
const card = new Card({});
card.setHideBorder(true);
document.body.innerHTML = card.render(``);
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"stroke-opacity",
"0"
);
});
it("should not hide border", () => {
const card = new Card({});
card.setHideBorder(false);
document.body.innerHTML = card.render(``);
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"stroke-opacity",
"1"
);
});
it("should hide title", () => {
const card = new Card({});
card.setHideTitle(true);
document.body.innerHTML = card.render(``);
expect(queryByTestId(document.body, "card-title")).toBeNull();
});
it("should not hide title", () => {
const card = new Card({});
card.setHideTitle(false);
document.body.innerHTML = card.render(``);
expect(queryByTestId(document.body, "card-title")).toBeInTheDocument();
});
it("title should have prefix icon", () => {
const card = new Card({ title: "ok", titlePrefixIcon: icons.contribs });
document.body.innerHTML = card.render(``);
expect(document.getElementsByClassName("icon")[0]).toBeInTheDocument();
});
it("title should not have prefix icon", () => {
const card = new Card({ title: "ok" });
document.body.innerHTML = card.render(``);
expect(document.getElementsByClassName("icon")[0]).toBeUndefined();
});
it("should have proper height, width", () => {
const card = new Card({ height: 200, width: 200, title: "ok" });
document.body.innerHTML = card.render(``);
expect(document.getElementsByTagName("svg")[0]).toHaveAttribute(
"height",
"200"
);
expect(document.getElementsByTagName("svg")[0]).toHaveAttribute(
"height",
"200"
);
});
it("should have less height after title is hidden", () => {
const card = new Card({ height: 200, title: "ok" });
card.setHideTitle(true);
document.body.innerHTML = card.render(``);
expect(document.getElementsByTagName("svg")[0]).toHaveAttribute(
"height",
"170"
);
});
it("main-card-body should have proper when title is visible", () => {
const card = new Card({ height: 200 });
document.body.innerHTML = card.render(``);
expect(queryByTestId(document.body, "main-card-body")).toHaveAttribute(
"transform",
"translate(0, 55)"
);
});
it("main-card-body should have proper position after title is hidden", () => {
const card = new Card({ height: 200 });
card.setHideTitle(true);
document.body.innerHTML = card.render(``);
expect(queryByTestId(document.body, "main-card-body")).toHaveAttribute(
"transform",
"translate(0, 25)"
);
});
it("should render with correct colors", () => {
// returns theme based colors with proper overrides and defaults
const { titleColor, textColor, iconColor, bgColor } = getCardColors({
title_color: "f00",
icon_color: "0f0",
text_color: "00f",
bg_color: "fff",
theme: "default",
});
const card = new Card({
height: 200,
colors: {
titleColor,
textColor,
iconColor,
bgColor,
},
});
document.body.innerHTML = card.render(``);
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[".header"];
expect(headerClassStyles.fill).toBe("#f00");
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
"#fff"
);
});
});
+33 -2
View File
@@ -9,7 +9,10 @@ const data = {
user: {
name: "Anurag Hazra",
repositoriesContributedTo: { totalCount: 61 },
contributionsCollection: { totalCommitContributions: 100, restrictedContributionsCount: 50 },
contributionsCollection: {
totalCommitContributions: 100,
restrictedContributionsCount: 50,
},
pullRequests: { totalCount: 300 },
issues: { totalCount: 200 },
followers: { totalCount: 100 },
@@ -102,4 +105,32 @@ describe("Test fetchStats", () => {
rank,
});
});
});
it("should fetch total commits", async () => {
mock.onPost("https://api.github.com/graphql").reply(200, data);
mock
.onGet("https://api.github.com/search/commits?q=author:anuraghazra")
.reply(200, { total_count: 1000 });
let stats = await fetchStats("anuraghazra", true, true);
const rank = calculateRank({
totalCommits: 1000 + 150,
totalRepos: 5,
followers: 100,
contributions: 61,
stargazers: 400,
prs: 300,
issues: 200,
});
expect(stats).toStrictEqual({
contributedTo: 61,
name: "Anurag Hazra",
totalCommits: 1000 + 150,
totalIssues: 200,
totalPRs: 300,
totalStars: 400,
rank,
});
});
});
-43
View File
@@ -69,20 +69,6 @@ describe("Test renderStatsCard", () => {
expect(queryByTestId(document.body, "contribs")).toBeNull();
});
it("should hide_border", () => {
document.body.innerHTML = renderStatsCard(stats, { hide_border: true });
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"stroke-opacity",
"0"
);
document.body.innerHTML = renderStatsCard(stats, { hide_border: false });
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"stroke-opacity",
"1"
);
});
it("should hide_rank", () => {
document.body.innerHTML = renderStatsCard(stats, { hide_rank: true });
@@ -202,35 +188,6 @@ describe("Test renderStatsCard", () => {
);
});
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, {
-19
View File
@@ -98,25 +98,6 @@ describe("Test renderTopLanguages", () => {
expect(document.querySelector("svg")).toHaveAttribute("height", "245");
});
it("should hide_title", () => {
document.body.innerHTML = renderTopLanguages(langs, { hide_title: false });
expect(document.querySelector("svg")).toHaveAttribute("height", "205");
expect(queryByTestId(document.body, "lang-items")).toHaveAttribute(
"y",
"55"
);
// Lets hide now
document.body.innerHTML = renderTopLanguages(langs, { hide_title: true });
expect(document.querySelector("svg")).toHaveAttribute("height", "175");
expect(queryByTestId(document.body, "header")).not.toBeInTheDocument();
expect(queryByTestId(document.body, "lang-items")).toHaveAttribute(
"y",
"25"
);
});
it("should render with custom width set", () => {
document.body.innerHTML = renderTopLanguages(langs, {});