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 } - + ${icons.contribs} 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;