diff --git a/api/index.js b/api/index.js
index f9041b72..0a25e372 100644
--- a/api/index.js
+++ b/api/index.js
@@ -16,6 +16,7 @@ module.exports = async (req, res) => {
icon_color,
text_color,
bg_color,
+ theme,
} = req.query;
let stats;
@@ -40,6 +41,7 @@ module.exports = async (req, res) => {
icon_color,
text_color,
bg_color,
+ theme,
})
);
};
diff --git a/api/pin.js b/api/pin.js
index b79472fa..bebe529a 100644
--- a/api/pin.js
+++ b/api/pin.js
@@ -11,6 +11,7 @@ module.exports = async (req, res) => {
icon_color,
text_color,
bg_color,
+ theme,
show_owner,
} = req.query;
@@ -32,6 +33,7 @@ module.exports = async (req, res) => {
icon_color,
text_color,
bg_color,
+ theme,
show_owner: parseBoolean(show_owner),
})
);
diff --git a/readme.md b/readme.md
index 55e77dfd..d27d7d8a 100644
--- a/readme.md
+++ b/readme.md
@@ -33,6 +33,7 @@
- [GitHub Stats Card](#github-stats-card)
- [GitHub Extra Pins](#github-extra-pins)
+- [Themes](#themes)
- [Customization](#customization)
- [Deploy Yourself](#deploy-on-your-own-vercel-instance)
@@ -66,6 +67,22 @@ To enable icons, you can pass `show_icons=true` in the query param, like so:
![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true)
```
+### Themes
+
+With inbuilt themes you can customize the look of the card without doing any [manual customization](#customization).
+
+Use `?theme=THEME_NAME` parameter like so :-
+
+```md
+![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=radical)
+```
+
+#### All inbuilt themes :-
+
+dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula
+
+Check out more themes at [theme config file](./themes/index.js) & **you can also contribute new themes** if you like :D
+
### Customization
You can customize the appearance of your `Stats Card` or `Repo Card` however you want with URL params.
@@ -84,12 +101,9 @@ Customization Options:
| hide_border | boolean | hides the stats card border | false | N/A |
| show_owner | boolean | shows owner name in repo card | N/A | false |
| show_icons | boolean | shows icons | false | N/A |
+| theme | string | sets inbuilt theme | 'default' | 'default_repocard' |
-- You can also customize the cards to be compatible with dark mode
-
-```md
-![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&title_color=fff&icon_color=79ff97&text_color=9f9f9f&bg_color=151515)
-```
+---
### Demo
@@ -105,6 +119,12 @@ Customization Options:
![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=["issues"]&show_icons=true)
+- Themes
+
+Choose from any of the [default themes](#themes)
+
+![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=radical)
+
- Customizing stats card
![Anurag's github stats](https://github-readme-stats.vercel.app/api/?username=anuraghazra&show_icons=true&title_color=fff&icon_color=79ff97&text_color=9f9f9f&bg_color=151515)
@@ -113,6 +133,8 @@ Customization Options:
![Customized Card](https://github-readme-stats.vercel.app/api/pin?username=anuraghazra&repo=github-readme-stats&title_color=fff&icon_color=f9f9f9&text_color=9f9f9f&bg_color=151515)
+---
+
# GitHub Extra Pins
GitHub extra pins allow you to pin more than 6 repositories in your profile using a GitHub readme profile.
diff --git a/src/renderRepoCard.js b/src/renderRepoCard.js
index db2dbf5f..1c913397 100644
--- a/src/renderRepoCard.js
+++ b/src/renderRepoCard.js
@@ -1,7 +1,7 @@
const {
kFormatter,
encodeHTML,
- fallbackColor,
+ getCardColors,
FlexLayout,
} = require("../src/utils");
const icons = require("./icons");
@@ -16,7 +16,14 @@ const renderRepoCard = (repo, options = {}) => {
isArchived,
forkCount,
} = repo;
- const { title_color, icon_color, text_color, bg_color, show_owner } = options;
+ const {
+ title_color,
+ icon_color,
+ text_color,
+ bg_color,
+ show_owner,
+ theme = "default_repocard",
+ } = options;
const header = show_owner ? nameWithOwner : name;
const langName = primaryLanguage ? primaryLanguage.name : "Unspecified";
@@ -30,10 +37,14 @@ const renderRepoCard = (repo, options = {}) => {
desc = `${description.slice(0, 55)}..`;
}
- const titleColor = fallbackColor(title_color, "#2f80ed");
- const iconColor = fallbackColor(icon_color, "#586069");
- const textColor = fallbackColor(text_color, "#333");
- const bgColor = fallbackColor(bg_color, "#FFFEFE");
+ // returns theme based colors with proper overrides and defaults
+ const { titleColor, textColor, iconColor, bgColor } = getCardColors({
+ title_color,
+ icon_color,
+ text_color,
+ bg_color,
+ theme,
+ });
const totalStars = kFormatter(stargazers.totalCount);
const totalForks = kFormatter(forkCount);
@@ -82,7 +93,7 @@ const renderRepoCard = (repo, options = {}) => {
.archive-badge { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; }
.archive-badge rect { opacity: 0.2 }
-
+
diff --git a/src/renderStatsCard.js b/src/renderStatsCard.js
index 2ba27614..7f9df536 100644
--- a/src/renderStatsCard.js
+++ b/src/renderStatsCard.js
@@ -1,4 +1,4 @@
-const { kFormatter, fallbackColor, FlexLayout } = require("../src/utils");
+const { kFormatter, getCardColors, FlexLayout } = require("../src/utils");
const getStyles = require("./getStyles");
const icons = require("./icons");
@@ -44,14 +44,19 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
icon_color,
text_color,
bg_color,
+ theme = "default",
} = options;
const lheight = parseInt(line_height);
- const titleColor = fallbackColor(title_color, "#2f80ed");
- const iconColor = fallbackColor(icon_color, "#4c71f2");
- const textColor = fallbackColor(text_color, "#333");
- const bgColor = fallbackColor(bg_color, "#FFFEFE");
+ // returns theme based colors with proper overrides and defaults
+ const { titleColor, textColor, iconColor, bgColor } = getCardColors({
+ title_color,
+ icon_color,
+ text_color,
+ bg_color,
+ theme,
+ });
// Meta data for creating text nodes with createTextNode function
const STATS = {
@@ -127,7 +132,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
? ""
: `
{
return `
@@ -77,6 +78,40 @@ function FlexLayout({ items, gap, direction }) {
});
}
+// returns theme based colors with proper overrides and defaults
+function getCardColors({
+ title_color,
+ text_color,
+ icon_color,
+ bg_color,
+ theme,
+ fallbackTheme = "default",
+}) {
+ const defaultTheme = themes[fallbackTheme];
+ const selectedTheme = themes[theme] || defaultTheme;
+
+ // get the color provided by the user else the theme color
+ // finally if both colors are invalid fallback to default theme
+ const titleColor = fallbackColor(
+ title_color || selectedTheme.title_color,
+ "#" + defaultTheme.title_color
+ );
+ const iconColor = fallbackColor(
+ icon_color || selectedTheme.icon_color,
+ "#" + defaultTheme.icon_color
+ );
+ const textColor = fallbackColor(
+ text_color || selectedTheme.text_color,
+ "#" + defaultTheme.text_color
+ );
+ const bgColor = fallbackColor(
+ bg_color || selectedTheme.bg_color,
+ "#" + defaultTheme.bg_color
+ );
+
+ return { titleColor, iconColor, textColor, bgColor };
+}
+
module.exports = {
renderError,
kFormatter,
@@ -86,4 +121,5 @@ module.exports = {
parseBoolean,
fallbackColor,
FlexLayout,
+ getCardColors,
};
diff --git a/tests/renderRepoCard.test.js b/tests/renderRepoCard.test.js
index a962a29b..f94ee4a6 100644
--- a/tests/renderRepoCard.test.js
+++ b/tests/renderRepoCard.test.js
@@ -3,6 +3,7 @@ const cssToObject = require("css-to-object");
const renderRepoCard = require("../src/renderRepoCard");
const { queryByTestId } = require("@testing-library/dom");
+const themes = require("../themes");
const data_repo = {
repository: {
@@ -108,13 +109,13 @@ describe("Test renderRepoCard", () => {
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[".header"];
- const statClassStyles = stylesObject[".description"];
+ const descClassStyles = stylesObject[".description"];
const iconClassStyles = stylesObject[".icon"];
expect(headerClassStyles.fill).toBe("#2f80ed");
- expect(statClassStyles.fill).toBe("#333");
+ expect(descClassStyles.fill).toBe("#333");
expect(iconClassStyles.fill).toBe("#586069");
- expect(queryByTestId(document.body, "card-border")).toHaveAttribute(
+ expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
"#FFFEFE"
);
@@ -136,18 +137,63 @@ describe("Test renderRepoCard", () => {
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[".header"];
- const statClassStyles = stylesObject[".description"];
+ const descClassStyles = stylesObject[".description"];
const iconClassStyles = stylesObject[".icon"];
expect(headerClassStyles.fill).toBe(`#${customColors.title_color}`);
- expect(statClassStyles.fill).toBe(`#${customColors.text_color}`);
+ expect(descClassStyles.fill).toBe(`#${customColors.text_color}`);
expect(iconClassStyles.fill).toBe(`#${customColors.icon_color}`);
- expect(queryByTestId(document.body, "card-border")).toHaveAttribute(
+ expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
"#252525"
);
});
+ it("should render custom colors with themes", () => {
+ document.body.innerHTML = renderRepoCard(data_repo.repository, {
+ title_color: "5a0",
+ theme: "radical",
+ });
+
+ const styleTag = document.querySelector("style");
+ const stylesObject = cssToObject(styleTag.innerHTML);
+
+ const headerClassStyles = stylesObject[".header"];
+ const descClassStyles = stylesObject[".description"];
+ const iconClassStyles = stylesObject[".icon"];
+
+ expect(headerClassStyles.fill).toBe("#5a0");
+ expect(descClassStyles.fill).toBe(`#${themes.radical.text_color}`);
+ expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`);
+ expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
+ "fill",
+ `#${themes.radical.bg_color}`
+ );
+ });
+
+ it("should render custom colors with themes and fallback to default colors if invalid", () => {
+ document.body.innerHTML = renderRepoCard(data_repo.repository, {
+ title_color: "invalid color",
+ text_color: "invalid color",
+ theme: "radical",
+ });
+
+ const styleTag = document.querySelector("style");
+ const stylesObject = cssToObject(styleTag.innerHTML);
+
+ const headerClassStyles = stylesObject[".header"];
+ const descClassStyles = stylesObject[".description"];
+ const iconClassStyles = stylesObject[".icon"];
+
+ expect(headerClassStyles.fill).toBe(`#${themes.default.title_color}`);
+ expect(descClassStyles.fill).toBe(`#${themes.default.text_color}`);
+ expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`);
+ expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
+ "fill",
+ `#${themes.radical.bg_color}`
+ );
+ });
+
it("should render archive badge if repo is archived", () => {
document.body.innerHTML = renderRepoCard({
...data_repo.repository,
@@ -176,7 +222,7 @@ describe("Test renderRepoCard", () => {
expect(queryByTestId(document.body, "stargazers")).toBeDefined();
expect(queryByTestId(document.body, "forkcount")).toBeNull();
-
+
document.body.innerHTML = renderRepoCard({
...data_repo.repository,
stargazers: { totalCount: 0 },
diff --git a/tests/renderStatsCard.test.js b/tests/renderStatsCard.test.js
index 603af0f4..f1c337c8 100644
--- a/tests/renderStatsCard.test.js
+++ b/tests/renderStatsCard.test.js
@@ -7,6 +7,7 @@ const {
queryByTestId,
queryAllByTestId,
} = require("@testing-library/dom");
+const themes = require("../themes");
describe("Test renderStatsCard", () => {
const stats = {
@@ -34,7 +35,7 @@ describe("Test renderStatsCard", () => {
expect(getByTestId(document.body, "issues").textContent).toBe("300");
expect(getByTestId(document.body, "prs").textContent).toBe("400");
expect(getByTestId(document.body, "contribs").textContent).toBe("500");
- expect(queryByTestId(document.body, "card-border")).toBeInTheDocument();
+ expect(queryByTestId(document.body, "card-bg")).toBeInTheDocument();
expect(queryByTestId(document.body, "rank-circle")).toBeInTheDocument();
});
@@ -57,7 +58,7 @@ describe("Test renderStatsCard", () => {
it("should hide_border", () => {
document.body.innerHTML = renderStatsCard(stats, { hide_border: true });
- expect(queryByTestId(document.body, "card-border")).not.toBeInTheDocument();
+ expect(queryByTestId(document.body, "card-bg")).not.toBeInTheDocument();
});
it("should hide_rank", () => {
@@ -79,7 +80,7 @@ describe("Test renderStatsCard", () => {
expect(headerClassStyles.fill).toBe("#2f80ed");
expect(statClassStyles.fill).toBe("#333");
expect(iconClassStyles.fill).toBe("#4c71f2");
- expect(queryByTestId(document.body, "card-border")).toHaveAttribute(
+ expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
"#FFFEFE"
);
@@ -105,12 +106,57 @@ describe("Test renderStatsCard", () => {
expect(headerClassStyles.fill).toBe(`#${customColors.title_color}`);
expect(statClassStyles.fill).toBe(`#${customColors.text_color}`);
expect(iconClassStyles.fill).toBe(`#${customColors.icon_color}`);
- expect(queryByTestId(document.body, "card-border")).toHaveAttribute(
+ expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
"#252525"
);
});
+ it("should render custom colors with themes", () => {
+ document.body.innerHTML = renderStatsCard(stats, {
+ title_color: "5a0",
+ theme: "radical",
+ });
+
+ const styleTag = document.querySelector("style");
+ const stylesObject = cssToObject(styleTag.innerHTML);
+
+ const headerClassStyles = stylesObject[".header"];
+ const statClassStyles = stylesObject[".stat"];
+ const iconClassStyles = stylesObject[".icon"];
+
+ expect(headerClassStyles.fill).toBe("#5a0");
+ expect(statClassStyles.fill).toBe(`#${themes.radical.text_color}`);
+ expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`);
+ expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
+ "fill",
+ `#${themes.radical.bg_color}`
+ );
+ });
+
+ it("should render custom colors with themes and fallback to default colors if invalid", () => {
+ document.body.innerHTML = renderStatsCard(stats, {
+ title_color: "invalid color",
+ text_color: "invalid color",
+ theme: "radical",
+ });
+
+ const styleTag = document.querySelector("style");
+ const stylesObject = cssToObject(styleTag.innerHTML);
+
+ const headerClassStyles = stylesObject[".header"];
+ const statClassStyles = stylesObject[".stat"];
+ const iconClassStyles = stylesObject[".icon"];
+
+ expect(headerClassStyles.fill).toBe(`#${themes.default.title_color}`);
+ expect(statClassStyles.fill).toBe(`#${themes.default.text_color}`);
+ expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`);
+ expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
+ "fill",
+ `#${themes.radical.bg_color}`
+ );
+ });
+
it("should hide the title", () => {
document.body.innerHTML = renderStatsCard(stats, {
hide_title: true,
diff --git a/tests/utils.test.js b/tests/utils.test.js
index 3584bb5d..765fd090 100644
--- a/tests/utils.test.js
+++ b/tests/utils.test.js
@@ -3,6 +3,7 @@ const {
encodeHTML,
renderError,
FlexLayout,
+ getCardColors,
} = require("../src/utils");
describe("Test utils.js", () => {
@@ -49,4 +50,48 @@ describe("Test utils.js", () => {
`12`
);
});
+
+ it("getCardColors: should return expected values", () => {
+ let colors = getCardColors({
+ title_color: "f00",
+ text_color: "0f0",
+ icon_color: "00f",
+ bg_color: "fff",
+ theme: "dark",
+ });
+ expect(colors).toStrictEqual({
+ titleColor: "#f00",
+ textColor: "#0f0",
+ iconColor: "#00f",
+ bgColor: "#fff",
+ });
+ });
+
+ it("getCardColors: should fallback to default colors if color is invalid", () => {
+ let colors = getCardColors({
+ title_color: "invalidcolor",
+ text_color: "0f0",
+ icon_color: "00f",
+ bg_color: "fff",
+ theme: "dark",
+ });
+ expect(colors).toStrictEqual({
+ titleColor: "#2f80ed",
+ textColor: "#0f0",
+ iconColor: "#00f",
+ bgColor: "#fff",
+ });
+ });
+
+ it("getCardColors: should fallback to specified theme colors if is not defined", () => {
+ let colors = getCardColors({
+ theme: "dark",
+ });
+ expect(colors).toStrictEqual({
+ titleColor: "#fff",
+ textColor: "#9f9f9f",
+ iconColor: "#79ff97",
+ bgColor: "#151515",
+ });
+ });
});
diff --git a/themes/index.js b/themes/index.js
new file mode 100644
index 00000000..7bc8c581
--- /dev/null
+++ b/themes/index.js
@@ -0,0 +1,76 @@
+const themes = {
+ default: {
+ title_color: "2f80ed",
+ icon_color: "4c71f2",
+ text_color: "333",
+ bg_color: "FFFEFE",
+ },
+ default_repocard: {
+ title_color: "2f80ed",
+ icon_color: "586069", // icon color is different
+ text_color: "333",
+ bg_color: "FFFEFE",
+ },
+ dark: {
+ title_color: "fff",
+ icon_color: "79ff97",
+ text_color: "9f9f9f",
+ bg_color: "151515",
+ },
+ radical: {
+ title_color: "fe428e",
+ icon_color: "f8d847",
+ text_color: "a9fef7",
+ bg_color: "141321",
+ },
+ merko: {
+ title_color: "abd200",
+ icon_color: "b7d364",
+ text_color: "68b587",
+ bg_color: "0a0f0b",
+ },
+ gruvbox: {
+ title_color: "fabd2f",
+ icon_color: "fe8019",
+ text_color: "8ec07c",
+ bg_color: "282828",
+ },
+ tokyonight: {
+ title_color: "70a5fd",
+ icon_color: "bf91f3",
+ text_color: "38bdae",
+ bg_color: "1a1b27",
+ },
+ onedark: {
+ title_color: "e4bf7a",
+ icon_color: "8eb573",
+ text_color: "df6d74",
+ bg_color: "282c34",
+ },
+ cobalt: {
+ title_color: "e683d9",
+ icon_color: "0480ef",
+ text_color: "75eeb2",
+ bg_color: "193549",
+ },
+ synthwave: {
+ title_color: "e2e9ec",
+ icon_color: "ef8539",
+ text_color: "e5289e",
+ bg_color: "2b213a",
+ },
+ highcontrast: {
+ title_color: "e7f216",
+ icon_color: "00ffff",
+ text_color: "fff",
+ bg_color: "000",
+ },
+ dracula: {
+ title_color: "ff6e96",
+ icon_color: "79dafa",
+ text_color: "f8f8f2",
+ bg_color: "282a36",
+ },
+};
+
+module.exports = themes;