diff --git a/api/gist.js b/api/gist.js index 1dbc5aeeddd53..4e0e4b2e6d380 100644 --- a/api/gist.js +++ b/api/gist.js @@ -22,6 +22,7 @@ export default async (req, res) => { border_color, show_owner, hide_border, + card_height, } = req.query; res.setHeader("Content-Type", "image/svg+xml"); @@ -74,6 +75,7 @@ export default async (req, res) => { locale: locale ? locale.toLowerCase() : null, show_owner: parseBoolean(show_owner), hide_border: parseBoolean(hide_border), + card_height, }), ); } catch (err) { diff --git a/api/index.js b/api/index.js index adfd33174cdbc..54cf4c505143b 100644 --- a/api/index.js +++ b/api/index.js @@ -38,6 +38,7 @@ export default async (req, res) => { border_color, rank_icon, show, + card_height, } = req.query; res.setHeader("Content-Type", "image/svg+xml"); @@ -102,6 +103,7 @@ export default async (req, res) => { disable_animations: parseBoolean(disable_animations), rank_icon, show: showStats, + card_height, }), ); } catch (err) { diff --git a/api/pin.js b/api/pin.js index 21ecf966b3ff4..159ba2ceaa2fa 100644 --- a/api/pin.js +++ b/api/pin.js @@ -24,6 +24,7 @@ export default async (req, res) => { locale, border_radius, border_color, + card_height, } = req.query; res.setHeader("Content-Type", "image/svg+xml"); @@ -80,6 +81,7 @@ export default async (req, res) => { border_color, show_owner: parseBoolean(show_owner), locale: locale ? locale.toLowerCase() : null, + card_height, }), ); } catch (err) { diff --git a/api/top-langs.js b/api/top-langs.js index d9bf6b09da01a..049f09dba35a1 100644 --- a/api/top-langs.js +++ b/api/top-langs.js @@ -33,6 +33,7 @@ export default async (req, res) => { border_color, disable_animations, hide_progress, + card_height, } = req.query; res.setHeader("Content-Type", "image/svg+xml"); @@ -96,6 +97,7 @@ export default async (req, res) => { locale: locale ? locale.toLowerCase() : null, disable_animations: parseBoolean(disable_animations), hide_progress: parseBoolean(hide_progress), + card_height, }), ); } catch (err) { diff --git a/api/wakatime.js b/api/wakatime.js index b2582caa5bd31..b381191d5a801 100644 --- a/api/wakatime.js +++ b/api/wakatime.js @@ -30,6 +30,7 @@ export default async (req, res) => { api_domain, border_radius, border_color, + card_height, } = req.query; res.setHeader("Content-Type", "image/svg+xml"); @@ -75,6 +76,7 @@ export default async (req, res) => { locale: locale ? locale.toLowerCase() : null, layout, langs_count, + card_height, }), ); } catch (err) { diff --git a/readme.md b/readme.md index 9658344302857..2addc9c180b62 100644 --- a/readme.md +++ b/readme.md @@ -298,6 +298,7 @@ You can customize the appearance of all your cards however you wish with URL par * `cache_seconds` - Sets the cache header manually *(min: 21600, max: 86400)*. Default: `21600 seconds (6 hours)`. * `locale` - Sets the language in the card, you can check full list of available locales [here](#available-locales). Default: `en`. * `border_radius` - Corner rounding on the card. Default: `4.5`. +* `card_height` - Card's height. (dynamically) > [!WARNING]\ > We use caching to decrease the load on our servers (see ). Our cards have a default cache of 6 hours (21600 seconds). Also, note that the cache is clamped to a minimum of 6 hours and a maximum of 24 hours. If you want the data on your statistics card to be updated more often you can [deploy your own instance](#deploy-on-your-own) and set [environment variable](#disable-rate-limit-protections) `CACHE_SECONDS` to a value of your choosing. diff --git a/src/cards/gist-card.js b/src/cards/gist-card.js index 9e889e74424cd..3bbfcc607e85d 100644 --- a/src/cards/gist-card.js +++ b/src/cards/gist-card.js @@ -10,6 +10,7 @@ import { flexLayout, iconWithLabel, createLanguageNode, + getAppropriateHeight, } from "../common/utils.js"; import Card from "../common/Card.js"; import { icons } from "../common/icons.js"; @@ -52,6 +53,7 @@ const renderGistCard = (gistData, options = {}) => { theme, border_radius, border_color, + card_height, show_owner = false, hide_border = false, } = options; @@ -77,8 +79,9 @@ const renderGistCard = (gistData, options = {}) => { .join(""); const lineHeight = descriptionLines > 3 ? 12 : 10; - const height = + const minHeight = (descriptionLines > 1 ? 120 : 110) + descriptionLines * lineHeight; + const height = getAppropriateHeight(card_height, minHeight); const totalStars = kFormatter(starsCount); const totalForks = kFormatter(forksCount); diff --git a/src/cards/repo-card.js b/src/cards/repo-card.js index 09b5841880a97..7b8ae37cbc5e3 100644 --- a/src/cards/repo-card.js +++ b/src/cards/repo-card.js @@ -12,6 +12,7 @@ import { wrapTextMultiline, iconWithLabel, createLanguageNode, + getAppropriateHeight, } from "../common/utils.js"; import { repoCardLocales } from "../translations.js"; @@ -71,6 +72,7 @@ const renderRepoCard = (repo, options = {}) => { show_owner = false, theme = "default_repocard", border_radius, + card_height, border_color, locale, } = options; @@ -87,8 +89,9 @@ const renderRepoCard = (repo, options = {}) => { .map((line) => `${encodeHTML(line)}`) .join(""); - const height = + const minHeight = (descriptionLines > 1 ? 120 : 110) + descriptionLines * lineHeight; + const height = getAppropriateHeight(card_height, minHeight); const i18n = new I18n({ locale, diff --git a/src/cards/stats-card.js b/src/cards/stats-card.js index 5f57205602016..05391c026feb7 100644 --- a/src/cards/stats-card.js +++ b/src/cards/stats-card.js @@ -6,6 +6,7 @@ import { CustomError, clampValue, flexLayout, + getAppropriateHeight, getCardColors, kFormatter, measureText, @@ -238,6 +239,7 @@ const renderStatsCard = (stats, options = {}) => { disable_animations = false, rank_icon = "default", show = [], + card_height, } = options; const lheight = parseInt(String(line_height), 10); @@ -391,10 +393,11 @@ const renderStatsCard = (stats, options = {}) => { // Calculate the card height depending on how many items there are // but if rank circle is visible clamp the minimum height to `150` - let height = Math.max( + const minHeight = Math.max( 45 + (statItems.length + 1) * lheight, hide_rank ? 0 : statItems.length ? 150 : 180, ); + const height = getAppropriateHeight(card_height, minHeight); // the lower the user's percentile the better const progress = 100 - rank.percentile; diff --git a/src/cards/top-languages-card.js b/src/cards/top-languages-card.js index 758bd34baff5d..77af3e71cb5c3 100644 --- a/src/cards/top-languages-card.js +++ b/src/cards/top-languages-card.js @@ -6,6 +6,7 @@ import { chunkArray, clampValue, flexLayout, + getAppropriateHeight, getCardColors, lowercaseTrim, measureText, @@ -738,6 +739,7 @@ const renderTopLanguages = (topLangs, options = {}) => { border_radius, border_color, disable_animations, + card_height, } = options; const i18n = new I18n({ @@ -805,7 +807,7 @@ const renderTopLanguages = (topLangs, options = {}) => { customTitle: custom_title, defaultTitle: i18n.t("langcard.title"), width, - height, + height: getAppropriateHeight(card_height, height), border_radius, colors, }); diff --git a/src/cards/types.d.ts b/src/cards/types.d.ts index dce964d21af7e..8a2ed2c848664 100644 --- a/src/cards/types.d.ts +++ b/src/cards/types.d.ts @@ -11,6 +11,7 @@ export type CommonOptions = { border_color: string; locale: string; hide_border: boolean; + card_height: number; }; export type StatCardOptions = CommonOptions & { diff --git a/src/cards/wakatime-card.js b/src/cards/wakatime-card.js index a6a203dad9c29..ccd465e75cadd 100644 --- a/src/cards/wakatime-card.js +++ b/src/cards/wakatime-card.js @@ -5,6 +5,7 @@ import { I18n } from "../common/I18n.js"; import { clampValue, flexLayout, + getAppropriateHeight, getCardColors, lowercaseTrim, } from "../common/utils.js"; @@ -218,6 +219,7 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { langs_count = languages.length, border_radius, border_color, + card_height, } = options; const shouldHideLangs = Array.isArray(hide) && hide.length > 0; @@ -259,7 +261,11 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { // Calculate the card height depending on how many items there are // but if rank circle is visible clamp the minimum height to `150` - let height = Math.max(45 + (filteredLanguages.length + 1) * lheight, 150); + const minHeight = Math.max( + 45 + (filteredLanguages.length + 1) * lheight, + 150, + ); + let height = getAppropriateHeight(card_height, minHeight); const cssStyles = getStyles({ titleColor, diff --git a/src/common/utils.js b/src/common/utils.js index 4fb473e2e3686..df6a95d7aec4b 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -552,6 +552,16 @@ const parseEmojis = (str) => { }); }; +/** + * Get the appropriate height + * + * @param {number | undefined} height height wanted + * @param {number} minHeight minimum height. + * @returns {number} appropriate height. + */ +const getAppropriateHeight = (height, minHeight) => + height && height > minHeight ? height : minHeight; + /** * Get diff in minutes between two dates. * @@ -592,4 +602,5 @@ export { chunkArray, parseEmojis, dateDiff, + getAppropriateHeight, }; diff --git a/tests/renderGistCard.test.js b/tests/renderGistCard.test.js index 6e636fc50de6b..b69497c7312fa 100644 --- a/tests/renderGistCard.test.js +++ b/tests/renderGistCard.test.js @@ -217,6 +217,21 @@ describe("test renderGistCard", () => { expect(queryByTestId(document.body, "forksCount")).toBeNull(); }); + it("should render custom height correctly", () => { + document.body.innerHTML = renderGistCard( + { + ...data, + }, + { + card_height: 280, + }, + ); + + expect( + document.body.getElementsByTagName("svg")[0].getAttribute("height"), + ).toBe("280"); + }); + it("should render without rounding", () => { document.body.innerHTML = renderGistCard(data, { border_radius: "0", diff --git a/tests/renderRepoCard.test.js b/tests/renderRepoCard.test.js index 61c9bc6da9ac5..a44974f0ea38b 100644 --- a/tests/renderRepoCard.test.js +++ b/tests/renderRepoCard.test.js @@ -319,6 +319,22 @@ describe("Test renderRepoCard", () => { expect(queryByTestId(document.body, "badge")).toHaveTextContent("模板"); }); + it("should render custom height correctly", () => { + document.body.innerHTML = renderRepoCard( + { + ...data_repo.repository, + isTemplate: true, + }, + { + card_height: 340, + }, + ); + + expect( + document.body.getElementsByTagName("svg")[0].getAttribute("height"), + ).toBe("340"); + }); + it("should render without rounding", () => { document.body.innerHTML = renderRepoCard(data_repo.repository, { border_radius: "0", diff --git a/tests/renderStatsCard.test.js b/tests/renderStatsCard.test.js index abbf2100306d7..be326aa85c0f6 100644 --- a/tests/renderStatsCard.test.js +++ b/tests/renderStatsCard.test.js @@ -372,6 +372,16 @@ describe("Test renderStatsCard", () => { ).toBe("287"); }); + it("should render custom height correctly", () => { + document.body.innerHTML = renderStatsCard(stats, { + card_height: 823, + }); + + expect( + document.body.getElementsByTagName("svg")[0].getAttribute("height"), + ).toBe("823"); + }); + it("should render translations", () => { document.body.innerHTML = renderStatsCard(stats, { locale: "cn" }); expect(document.getElementsByClassName("header")[0].textContent).toBe( diff --git a/tests/renderTopLanguagesCard.test.js b/tests/renderTopLanguagesCard.test.js index 6b7ef62aa30dd..408e3d2d43390 100644 --- a/tests/renderTopLanguagesCard.test.js +++ b/tests/renderTopLanguagesCard.test.js @@ -848,4 +848,17 @@ describe("Test renderTopLanguages", () => { "No languages data.", ); }); + + it("should render custom height correctly", () => { + document.body.innerHTML = renderTopLanguages( + {}, + { + card_height: 324, + }, + ); + + expect( + document.body.getElementsByTagName("svg")[0].getAttribute("height"), + ).toBe("324"); + }); }); diff --git a/tests/renderWakatimeCard.test.js b/tests/renderWakatimeCard.test.js index 5098a3ac4943a..5e812fe7ff29d 100644 --- a/tests/renderWakatimeCard.test.js +++ b/tests/renderWakatimeCard.test.js @@ -69,6 +69,20 @@ describe("Test Render Wakatime Card", () => { ); }); + it("should render custom height correctly", () => { + document.body.innerHTML = renderWakatimeCard( + { + ...wakaTimeData.data, + languages: undefined, + }, + { card_height: 432 }, + ); + + expect( + document.body.getElementsByTagName("svg")[0].getAttribute("height"), + ).toBe("432"); + }); + it('should show "no coding activity this week" message when using compact layout and there has not been activity', () => { document.body.innerHTML = renderWakatimeCard( { diff --git a/tests/utils.test.js b/tests/utils.test.js index 5bd43955485c3..c73c016af5645 100644 --- a/tests/utils.test.js +++ b/tests/utils.test.js @@ -7,6 +7,7 @@ import { parseBoolean, renderError, wrapTextMultiline, + getAppropriateHeight, } from "../src/common/utils.js"; import { expect, it, describe } from "@jest/globals"; @@ -134,6 +135,23 @@ describe("Test utils.js", () => { borderColor: "#fff", }); }); + + describe("getAppropriateHeight", () => { + it("should return the minimum height if the height is superior", () => { + let height = getAppropriateHeight(210, 289); + expect(height).toBe(289); + }); + + it("should return the minimum height if the height is undefined", () => { + let height = getAppropriateHeight(undefined, 289); + expect(height).toBe(289); + }); + + it("should return the custom height", () => { + let height = getAppropriateHeight(213, 100); + expect(height).toBe(213); + }); + }); }); describe("wrapTextMultiline", () => {