diff --git a/api/pin.js b/api/pin.js
index bede7d87f5972..5595394a5b460 100644
--- a/api/pin.js
+++ b/api/pin.js
@@ -5,6 +5,7 @@ import {
CONSTANTS,
parseBoolean,
renderError,
+ parseArray,
} from "../src/common/utils.js";
import { fetchRepo } from "../src/fetchers/repo-fetcher.js";
import { isLocaleAvailable } from "../src/translations.js";
@@ -25,6 +26,10 @@ export default async (req, res) => {
border_radius,
border_color,
description_lines_count,
+ show_lang_bar,
+ hide,
+ langs_count,
+ hide_progress,
} = req.query;
res.setHeader("Content-Type", "image/svg+xml");
@@ -83,6 +88,10 @@ export default async (req, res) => {
show_owner: parseBoolean(show_owner),
locale: locale ? locale.toLowerCase() : null,
description_lines_count,
+ show_lang_bar: parseBoolean(show_lang_bar),
+ hide: parseArray(hide),
+ langs_count,
+ hide_progress: parseBoolean(hide_progress),
}),
);
} catch (err) {
diff --git a/src/cards/repo-card.js b/src/cards/repo-card.js
index bbfda52d47778..074341a892b5a 100644
--- a/src/cards/repo-card.js
+++ b/src/cards/repo-card.js
@@ -15,11 +15,16 @@ import {
clampValue,
} from "../common/utils.js";
import { repoCardLocales } from "../translations.js";
+import {
+ renderCompactLayout,
+ trimTopLanguages,
+ getDefaultLanguagesCountByLayout,
+} from "./top-languages-card.js";
const ICON_SIZE = 16;
const DESCRIPTION_LINE_WIDTH = 59;
const DESCRIPTION_MAX_LINES = 3;
-
+const CARD_PADDING = 25;
/**
* Retrieves the repository description and wraps it to fit the card width.
*
@@ -42,6 +47,18 @@ const getBadgeSVG = (label, textColor) => `
`;
+/**
+ * Calculates extra height needed for the languages bar.
+ *
+ * @param {number} totalLangs Total number of languages.
+ * @param {boolean} hideProgress Flag to hide progress bar.
+ * @returns {number} Card height.
+ */
+const calculateExtraLangHeigth = (totalLangs, hideProgress) => {
+ const baseHeight = Math.ceil(totalLangs / 2) * 25;
+ return hideProgress ? baseHeight - 30 : baseHeight;
+};
+
/**
* @typedef {import("../fetchers/types").RepositoryData} RepositoryData Repository data.
* @typedef {import("./types").RepoCardOptions} RepoCardOptions Repo card options.
@@ -64,6 +81,7 @@ const renderRepoCard = (repo, options = {}) => {
isTemplate,
starCount,
forkCount,
+ languagesBreakdown,
} = repo;
const {
hide_border = false,
@@ -77,8 +95,19 @@ const renderRepoCard = (repo, options = {}) => {
border_color,
locale,
description_lines_count,
+ show_lang_bar = false,
+ hide_progress = false,
+ layout = "compact", // add more layouts in the future
+ langs_count = getDefaultLanguagesCountByLayout({ layout, hide_progress }),
+ hide,
} = options;
+ const { langs, totalLanguageSize } = trimTopLanguages(
+ languagesBreakdown,
+ langs_count,
+ hide,
+ );
+
const lineHeight = 10;
const header = show_owner ? nameWithOwner : name;
const langName = (primaryLanguage && primaryLanguage.name) || "Unspecified";
@@ -149,11 +178,22 @@ const renderRepoCard = (repo, options = {}) => {
gap: 25,
}).join("");
+ const languageBar = renderCompactLayout(
+ langs,
+ 400,
+ totalLanguageSize,
+ hide_progress,
+ );
+
const card = new Card({
defaultTitle: header.length > 35 ? `${header.slice(0, 35)}...` : header,
titlePrefixIcon: icons.contribs,
width: 400,
- height,
+ height:
+ height +
+ (show_lang_bar
+ ? calculateExtraLangHeigth(langs.length, hide_progress)
+ : 0),
border_radius,
colors,
});
@@ -167,8 +207,32 @@ const renderRepoCard = (repo, options = {}) => {
.icon { fill: ${colors.iconColor} }
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
.badge rect { opacity: 0.2 }
+ .lang-name { font: 400 11px "Segoe UI", Ubuntu, Sans-Serif; fill: ${colors.textColor} }
`);
+ if (show_lang_bar) {
+ return card.render(`
+ ${
+ isTemplate
+ ? // @ts-ignore
+ getBadgeSVG(i18n.t("repocard.template"), colors.textColor)
+ : isArchived
+ ? // @ts-ignore
+ getBadgeSVG(i18n.t("repocard.archived"), colors.textColor)
+ : ""
+ }
+
+
+ ${descriptionSvg}
+
+
+
+
+ `);
+ }
+
return card.render(`
${
isTemplate
@@ -187,6 +251,7 @@ const renderRepoCard = (repo, options = {}) => {
${starAndForkCount}
+
`);
};
diff --git a/src/cards/top-languages-card.js b/src/cards/top-languages-card.js
index 9385f4a7ebed3..749bffd290ab7 100644
--- a/src/cards/top-languages-card.js
+++ b/src/cards/top-languages-card.js
@@ -871,6 +871,7 @@ const renderTopLanguages = (topLangs, options = {}) => {
};
export {
+ renderCompactLayout,
getLongestLang,
degreesToRadians,
radiansToDegrees,
diff --git a/src/cards/types.d.ts b/src/cards/types.d.ts
index 9a21be4a0160a..5e47a74cf0702 100644
--- a/src/cards/types.d.ts
+++ b/src/cards/types.d.ts
@@ -33,6 +33,11 @@ export type StatCardOptions = CommonOptions & {
export type RepoCardOptions = CommonOptions & {
show_owner: boolean;
description_lines_count: number;
+ show_lang_bar: boolean;
+ hide_progress: boolean;
+ layout: "compact";
+ langs_count: number;
+ hide: string[];
};
export type TopLangOptions = CommonOptions & {
diff --git a/src/fetchers/repo-fetcher.js b/src/fetchers/repo-fetcher.js
index 6438f8895cfb6..5acb72cc1d401 100644
--- a/src/fetchers/repo-fetcher.js
+++ b/src/fetchers/repo-fetcher.js
@@ -55,6 +55,38 @@ const fetcher = (variables, token) => {
},
);
};
+/**
+ * Language data fetcher.
+ *
+ * @param {AxiosRequestHeaders} variables Fetcher variables.
+ * @param {string} token GitHub token.
+ * @returns {Promise} The response.
+ */
+const fetcherLanguage = (variables, token) => {
+ return request(
+ {
+ query: `
+ query getRepoLanguages($login: String!, $repo: String!) {
+ repository(owner: $login, name: $repo) {
+ languages(first: 10) {
+ edges {
+ node {
+ name
+ color
+ }
+ size
+ }
+ }
+ }
+ }
+ `,
+ variables,
+ },
+ {
+ Authorization: `token ${token}`,
+ },
+ );
+};
const urlExample = "/api/pin?username=USERNAME&repo=REPO_NAME";
@@ -91,6 +123,30 @@ const fetchRepo = async (username, reponame) => {
const isUser = data.organization === null && data.user;
const isOrg = data.user === null && data.organization;
+ let resLanguage = await retryer(fetcherLanguage, {
+ login: username,
+ repo: reponame,
+ });
+
+ const data_languages = resLanguage.data.data;
+
+ const toplanguages = data_languages.repository.languages.edges.reduce(
+ (acc, edge) => {
+ const { name, color } = edge.node;
+ const size = edge.size;
+
+ if (acc[name]) {
+ acc[name].size += size;
+ acc[name].count += 1;
+ } else {
+ acc[name] = { name, color, size, count: 1 };
+ }
+
+ return acc;
+ },
+ {},
+ );
+
if (isUser) {
if (!data.user.repository || data.user.repository.isPrivate) {
throw new Error("User Repository Not found");
@@ -98,6 +154,7 @@ const fetchRepo = async (username, reponame) => {
return {
...data.user.repository,
starCount: data.user.repository.stargazers.totalCount,
+ languagesBreakdown: toplanguages,
};
}
@@ -111,6 +168,7 @@ const fetchRepo = async (username, reponame) => {
return {
...data.organization.repository,
starCount: data.organization.repository.stargazers.totalCount,
+ languagesBreakdown: toplanguages,
};
}
diff --git a/src/fetchers/types.d.ts b/src/fetchers/types.d.ts
index affb407b816b0..31dc927c7e793 100644
--- a/src/fetchers/types.d.ts
+++ b/src/fetchers/types.d.ts
@@ -20,6 +20,7 @@ export type RepositoryData = {
id: string;
name: string;
};
+ languagesBreakdown: TopLangData;
forkCount: number;
starCount: number;
};
diff --git a/tests/fetchRepo.test.js b/tests/fetchRepo.test.js
index a980917f3d628..44e762923c452 100644
--- a/tests/fetchRepo.test.js
+++ b/tests/fetchRepo.test.js
@@ -18,6 +18,29 @@ const data_repo = {
},
};
+const data_languages = {
+ languagesBreakdown: {
+ HTML: {
+ color: "#0f0",
+ name: "HTML",
+ size: 200,
+ count: 1,
+ },
+ javascript: {
+ color: "#0ff",
+ name: "javascript",
+ size: 200,
+ count: 1,
+ },
+ css: {
+ color: "#ff0",
+ name: "css",
+ size: 100,
+ count: 1,
+ },
+ },
+};
+
const data_user = {
data: {
user: { repository: data_repo.repository },
@@ -32,6 +55,19 @@ const data_org = {
},
};
+const data_languages_resp = {
+ data: {
+ repository: {
+ languages: {
+ edges: [
+ { node: { name: "HTML", color: "#0f0" }, size: 200 },
+ { node: { name: "javascript", color: "#0ff" }, size: 200 },
+ { node: { name: "css", color: "#ff0" }, size: 100 },
+ ],
+ },
+ },
+ },
+};
const mock = new MockAdapter(axios);
afterEach(() => {
@@ -40,30 +76,42 @@ afterEach(() => {
describe("Test fetchRepo", () => {
it("should fetch correct user repo", async () => {
- mock.onPost("https://api.github.com/graphql").reply(200, data_user);
+ mock.onPost("https://api.github.com/graphql").replyOnce(200, data_user);
+ mock
+ .onPost("https://api.github.com/graphql")
+ .replyOnce(200, data_languages_resp);
let repo = await fetchRepo("anuraghazra", "convoychat");
+ console.log(repo);
expect(repo).toStrictEqual({
...data_repo.repository,
+ languagesBreakdown: data_languages.languagesBreakdown,
starCount: data_repo.repository.stargazers.totalCount,
});
});
it("should fetch correct org repo", async () => {
- mock.onPost("https://api.github.com/graphql").reply(200, data_org);
+ mock.onPost("https://api.github.com/graphql").replyOnce(200, data_org);
+ mock
+ .onPost("https://api.github.com/graphql")
+ .replyOnce(200, data_languages_resp);
let repo = await fetchRepo("anuraghazra", "convoychat");
expect(repo).toStrictEqual({
...data_repo.repository,
starCount: data_repo.repository.stargazers.totalCount,
+ languagesBreakdown: data_languages.languagesBreakdown,
});
});
it("should throw error if user is found but repo is null", async () => {
+ mock.onPost("https://api.github.com/graphql").replyOnce(200, {
+ data: { user: { repository: null }, organization: null },
+ });
mock
.onPost("https://api.github.com/graphql")
- .reply(200, { data: { user: { repository: null }, organization: null } });
+ .replyOnce(200, data_languages_resp);
await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow(
"User Repository Not found",
@@ -71,9 +119,12 @@ describe("Test fetchRepo", () => {
});
it("should throw error if org is found but repo is null", async () => {
+ mock.onPost("https://api.github.com/graphql").replyOnce(200, {
+ data: { user: null, organization: { repository: null } },
+ });
mock
.onPost("https://api.github.com/graphql")
- .reply(200, { data: { user: null, organization: { repository: null } } });
+ .replyOnce(200, data_languages_resp);
await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow(
"Organization Repository Not found",
@@ -83,7 +134,10 @@ describe("Test fetchRepo", () => {
it("should throw error if both user & org data not found", async () => {
mock
.onPost("https://api.github.com/graphql")
- .reply(200, { data: { user: null, organization: null } });
+ .replyOnce(200, { data: { user: null, organization: null } });
+ mock
+ .onPost("https://api.github.com/graphql")
+ .replyOnce(200, data_languages_resp);
await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow(
"Not found",
@@ -91,12 +145,15 @@ describe("Test fetchRepo", () => {
});
it("should throw error if repository is private", async () => {
- mock.onPost("https://api.github.com/graphql").reply(200, {
+ mock.onPost("https://api.github.com/graphql").replyOnce(200, {
data: {
user: { repository: { ...data_repo, isPrivate: true } },
organization: null,
},
});
+ mock
+ .onPost("https://api.github.com/graphql")
+ .replyOnce(200, data_languages_resp);
await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow(
"User Repository Not found",
diff --git a/tests/pin.test.js b/tests/pin.test.js
index 2583ddfe9e9af..daad7ad1fe925 100644
--- a/tests/pin.test.js
+++ b/tests/pin.test.js
@@ -22,6 +22,23 @@ const data_repo = {
},
forkCount: 100,
isTemplate: false,
+ languagesBreakdown: {
+ HTML: {
+ color: "#0f0",
+ name: "HTML",
+ size: 200,
+ },
+ javascript: {
+ color: "#0ff",
+ name: "javascript",
+ size: 200,
+ },
+ css: {
+ color: "#ff0",
+ name: "css",
+ size: 100,
+ },
+ },
},
};
@@ -31,6 +48,19 @@ const data_user = {
organization: null,
},
};
+const data_languages_resp = {
+ data: {
+ repository: {
+ languages: {
+ edges: [
+ { node: { name: "HTML", color: "#0f0" }, size: 200 },
+ { node: { name: "javascript", color: "#0ff" }, size: 200 },
+ { node: { name: "css", color: "#ff0" }, size: 100 },
+ ],
+ },
+ },
+ },
+};
const mock = new MockAdapter(axios);
@@ -50,8 +80,10 @@ describe("Test /api/pin", () => {
setHeader: jest.fn(),
send: jest.fn(),
};
- mock.onPost("https://api.github.com/graphql").reply(200, data_user);
-
+ mock.onPost("https://api.github.com/graphql").replyOnce(200, data_user);
+ mock
+ .onPost("https://api.github.com/graphql")
+ .replyOnce(200, data_languages_resp);
await pin(req, res);
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
@@ -79,7 +111,11 @@ describe("Test /api/pin", () => {
setHeader: jest.fn(),
send: jest.fn(),
};
- mock.onPost("https://api.github.com/graphql").reply(200, data_user);
+ // To Api calls
+ mock.onPost("https://api.github.com/graphql").replyOnce(200, data_user);
+ mock
+ .onPost("https://api.github.com/graphql")
+ .replyOnce(200, data_languages_resp);
await pin(req, res);
@@ -106,9 +142,12 @@ describe("Test /api/pin", () => {
setHeader: jest.fn(),
send: jest.fn(),
};
+ mock.onPost("https://api.github.com/graphql").replyOnce(200, {
+ data: { user: { repository: null }, organization: null },
+ });
mock
.onPost("https://api.github.com/graphql")
- .reply(200, { data: { user: { repository: null }, organization: null } });
+ .replyOnce(200, data_languages_resp);
await pin(req, res);
@@ -127,9 +166,12 @@ describe("Test /api/pin", () => {
setHeader: jest.fn(),
send: jest.fn(),
};
+ mock.onPost("https://api.github.com/graphql").replyOnce(200, {
+ data: { user: null, organization: { repository: null } },
+ });
mock
.onPost("https://api.github.com/graphql")
- .reply(200, { data: { user: null, organization: { repository: null } } });
+ .replyOnce(200, data_languages_resp);
await pin(req, res);
@@ -150,7 +192,10 @@ describe("Test /api/pin", () => {
setHeader: jest.fn(),
send: jest.fn(),
};
- mock.onPost("https://api.github.com/graphql").reply(200, data_user);
+ mock.onPost("https://api.github.com/graphql").replyOnce(200, data_user);
+ mock
+ .onPost("https://api.github.com/graphql")
+ .replyOnce(200, data_languages_resp);
await pin(req, res);
@@ -172,7 +217,10 @@ describe("Test /api/pin", () => {
setHeader: jest.fn(),
send: jest.fn(),
};
- mock.onPost("https://api.github.com/graphql").reply(200, data_user);
+ mock.onPost("https://api.github.com/graphql").replyOnce(200, data_user);
+ mock
+ .onPost("https://api.github.com/graphql")
+ .replyOnce(200, data_languages_resp);
await pin(req, res);
diff --git a/tests/renderRepoCard.test.js b/tests/renderRepoCard.test.js
index abbad4dbe2a3b..e78cbfd1cadbd 100644
--- a/tests/renderRepoCard.test.js
+++ b/tests/renderRepoCard.test.js
@@ -18,6 +18,23 @@ const data_repo = {
},
starCount: 38000,
forkCount: 100,
+ languagesBreakdown: {
+ HTML: {
+ color: "#0f0",
+ name: "HTML",
+ size: 200,
+ },
+ javascript: {
+ color: "#0ff",
+ name: "javascript",
+ size: 200,
+ },
+ css: {
+ color: "#ff0",
+ name: "css",
+ size: 100,
+ },
+ },
},
};