diff --git a/api/top-langs.js b/api/top-langs.js
index 840f88b..a758e33 100644
--- a/api/top-langs.js
+++ b/api/top-langs.js
@@ -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,
})
);
};
diff --git a/readme.md b/readme.md
index caaecf7..e4fc3ae 100644
--- a/readme.md
+++ b/readme.md
@@ -137,6 +137,7 @@ Customization Options:
| 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' |
+| card_width | number | set the card width | N/A | N/A | 300 |
> 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
diff --git a/src/Card.js b/src/Card.js
new file mode 100644
index 0000000..c8b21b6
--- /dev/null
+++ b/src/Card.js
@@ -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 = `
+
+ `;
+
+ const prefixIcon = `
+
+ `;
+ return `
+
+ ${FlexLayout({
+ items: [this.titlePrefixIcon && prefixIcon, titleText],
+ gap: 25,
+ }).join("")}
+
+ `;
+ }
+
+ render(body) {
+ return `
+
+ `;
+ }
+}
+
+module.exports = Card;
diff --git a/src/getStyles.js b/src/getStyles.js
index 1012624..b15f46d 100644
--- a/src/getStyles.js
+++ b/src/getStyles.js
@@ -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 };
diff --git a/src/renderRepoCard.js b/src/renderRepoCard.js
index 8f9f5b6..9721ebf 100644
--- a/src/renderRepoCard.js
+++ b/src/renderRepoCard.js
@@ -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 `
+
+ ${label}
+ `;
+ };
const svgStars =
stargazers.totalCount > 0 &&
- `
-
- ${totalStars}
- `;
-
+ iconWithLabel(icons.star, totalStars, "stargazers");
const svgForks =
- forkCount > 0 &&
- `
-
- ${totalForks}
- `;
+ forkCount > 0 && iconWithLabel(icons.fork, totalForks, "forkcount");
- return `
-
- `;
+
+ `);
};
module.exports = renderRepoCard;
diff --git a/src/renderStatsCard.js b/src/renderStatsCard.js
index 7e9d0cb..ace6bcb 100644
--- a/src/renderStatsCard.js
+++ b/src/renderStatsCard.js
@@ -4,8 +4,9 @@ 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 kValue = kFormatter(value);
@@ -52,7 +53,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({
@@ -116,44 +117,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
- ? ""
- : ``;
-
- const border = `
-
- `;
-
const rankCircle = hide_rank
? ""
- : `
+ : `
@@ -169,34 +137,45 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
`;
- 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 `
-
-
-
- ${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,
+ },
+ });
-
- ${rankCircle}
+ card.setHideBorder(hide_border);
+ card.setHideTitle(hide_title);
+ card.setCSS(cssStyles);
-
- ${FlexLayout({
- items: statItems,
- gap: lheight,
- direction: "column",
- }).join("")}
-
-
-
- `;
+ return card.render(`
+ ${rankCircle}
+
+
+ ${FlexLayout({
+ items: statItems,
+ gap: lheight,
+ direction: "column",
+ }).join("")}
+
+ `);
};
module.exports = renderStatsCard;
diff --git a/src/renderTopLanguages.js b/src/renderTopLanguages.js
index 21594df..5ccb2e0 100644
--- a/src/renderTopLanguages.js
+++ b/src/renderTopLanguages.js
@@ -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 `
-
-
-
+ 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
- ? ""
- : ``
- }
-
-
- ${finalLayout}
-
+ return card.render(`
+
+ ${finalLayout}
- `;
+ `);
};
module.exports = renderTopLanguages;
diff --git a/tests/card.test.js b/tests/card.test.js
new file mode 100644
index 0000000..318cf56
--- /dev/null
+++ b/tests/card.test.js
@@ -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"
+ );
+ });
+});
diff --git a/tests/renderStatsCard.test.js b/tests/renderStatsCard.test.js
index 04cdadb..983a406 100644
--- a/tests/renderStatsCard.test.js
+++ b/tests/renderStatsCard.test.js
@@ -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, {
diff --git a/tests/renderTopLanguages.test.js b/tests/renderTopLanguages.test.js
index a6f94a8..9996038 100644
--- a/tests/renderTopLanguages.test.js
+++ b/tests/renderTopLanguages.test.js
@@ -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, {});