From 62d65ab483fedbcf9499d57ab8ccef0df9f85165 Mon Sep 17 00:00:00 2001 From: Anurag Hazra Date: Sun, 26 Sep 2021 21:02:27 +0530 Subject: [PATCH] refactor: refactor repo card (#1325) * refactor: refactored repo-card * test: fix tests * test: fixed tests * fix: unprovided description error --- api/pin.js | 2 +- src/cards/repo-card.js | 129 +++++++++++++++++++---------------- src/common/utils.js | 23 +++++-- src/fetchers/repo-fetcher.js | 10 ++- tests/fetchRepo.test.js | 15 ++-- tests/pin.test.js | 19 +++++- tests/renderRepoCard.test.js | 19 ++++-- 7 files changed, 140 insertions(+), 77 deletions(-) diff --git a/api/pin.js b/api/pin.js index 7fad6c09f3746..65d09f0ec36e8 100644 --- a/api/pin.js +++ b/api/pin.js @@ -53,7 +53,7 @@ module.exports = async (req, res) => { and if both are zero we are not showing the stats so we can just make the cache longer, since there is no need to frequent updates */ - const stars = repoData.stargazers.totalCount; + const stars = repoData.starCount; const forks = repoData.forkCount; const isBothOver1K = stars > 1000 && forks > 1000; const isBothUnder1 = stars < 1 && forks < 1; diff --git a/src/cards/repo-card.js b/src/cards/repo-card.js index b1112d64c8f34..4573fcf6a2a94 100644 --- a/src/cards/repo-card.js +++ b/src/cards/repo-card.js @@ -1,4 +1,3 @@ -const toEmoji = require("emoji-name-map"); const { kFormatter, encodeHTML, @@ -6,21 +5,75 @@ const { flexLayout, wrapTextMultiline, measureText, + parseEmojis, } = require("../common/utils"); const I18n = require("../common/I18n"); const Card = require("../common/Card"); const icons = require("../common/icons"); const { repoCardLocales } = require("../translations"); +/** + * @param {string} label + * @param {string} textColor + * @returns {string} + */ +const getBadgeSVG = (label, textColor) => ` + + + + ${label} + + +`; + +/** + * @param {string} langName + * @param {string} langColor + * @returns {string} + */ +const createLanguageNode = (langName, langColor) => { + return ` + + + ${langName} + + `; +}; + +const ICON_SIZE = 16; +const iconWithLabel = (icon, label, testid) => { + if (label <= 0) return ""; + const iconSvg = ` + + ${icon} + + `; + const text = `${label}`; + return flexLayout({ items: [iconSvg, text], gap: 20 }).join(""); +}; + const renderRepoCard = (repo, options = {}) => { const { name, nameWithOwner, description, primaryLanguage, - stargazers, isArchived, isTemplate, + starCount, forkCount, } = repo; const { @@ -36,22 +89,17 @@ const renderRepoCard = (repo, options = {}) => { locale, } = options; + const lineHeight = 10; const header = show_owner ? nameWithOwner : name; const langName = (primaryLanguage && primaryLanguage.name) || "Unspecified"; const langColor = (primaryLanguage && primaryLanguage.color) || "#333"; - const shiftText = langName.length > 15 ? 0 : 30; - - let desc = description || "No description provided"; - - // parse emojis to unicode - desc = desc.replace(/:\w+:/gm, (emoji) => { - return toEmoji.get(emoji) || ""; - }); - + const desc = parseEmojis(description || "No description provided"); const multiLineDescription = wrapTextMultiline(desc); const descriptionLines = multiLineDescription.length; - const lineHeight = 10; + const descriptionSvg = multiLineDescription + .map((line) => `${encodeHTML(line)}`) + .join(""); const height = (descriptionLines > 1 ? 120 : 110) + descriptionLines * lineHeight; @@ -72,56 +120,21 @@ const renderRepoCard = (repo, options = {}) => { theme, }); - const totalStars = kFormatter(stargazers.totalCount); - const totalForks = kFormatter(forkCount); - - const getBadgeSVG = (label) => ` - - - - ${label} - - - `; - const svgLanguage = primaryLanguage - ? ` - - - ${langName} - - ` + ? createLanguageNode(langName, langColor) : ""; - const iconSize = 16; - const iconWithLabel = (icon, label, testid) => { - const iconSvg = ` - - ${icon} - - `; - const text = `${label}`; - return flexLayout({ items: [iconSvg, text], gap: 20 }).join(""); - }; - - const svgStars = - stargazers.totalCount > 0 && - iconWithLabel(icons.star, totalStars, "stargazers"); - const svgForks = - forkCount > 0 && iconWithLabel(icons.fork, totalForks, "forkcount"); + const totalStars = kFormatter(starCount); + const totalForks = kFormatter(forkCount); + const svgStars = iconWithLabel(icons.star, totalStars, "stargazers"); + const svgForks = iconWithLabel(icons.fork, totalForks, "forkcount"); const starAndForkCount = flexLayout({ items: [svgLanguage, svgStars, svgForks], sizes: [ measureText(langName, 12), - iconSize + measureText(`${totalStars}`, 12), - iconSize + measureText(`${totalForks}`, 12), + ICON_SIZE + measureText(`${totalStars}`, 12), + ICON_SIZE + measureText(`${totalForks}`, 12), ], gap: 25, }).join(""); @@ -155,16 +168,14 @@ const renderRepoCard = (repo, options = {}) => { return card.render(` ${ isTemplate - ? getBadgeSVG(i18n.t("repocard.template")) + ? getBadgeSVG(i18n.t("repocard.template"), textColor) : isArchived - ? getBadgeSVG(i18n.t("repocard.archived")) + ? getBadgeSVG(i18n.t("repocard.archived"), textColor) : "" } - ${multiLineDescription - .map((line) => `${encodeHTML(line)}`) - .join("")} + ${descriptionSvg} diff --git a/src/common/utils.js b/src/common/utils.js index f14e8cc65f4f0..7834dbae2cb0e 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -1,6 +1,7 @@ const axios = require("axios"); const wrap = require("word-wrap"); const themes = require("../../themes"); +const toEmoji = require("emoji-name-map"); const renderError = (message, secondaryMessage = "") => { return ` @@ -88,10 +89,11 @@ function request(data, headers) { } /** - * - * @param {string[]} items - * @param {Number} gap - * @param {"column" | "row"} direction + * @param {object} props + * @param {string[]} props.items + * @param {number} props.gap + * @param {number[]} props.sizes + * @param {"column" | "row"} props.direction * * @returns {string[]} * @@ -257,6 +259,18 @@ function chunkArray(arr, perChunk) { }, []); } +/** + * + * @param {string} str + * @returns {string} + */ +function parseEmojis(str) { + if (!str) throw new Error("[parseEmoji]: str argument not provided"); + return str.replace(/:\w+:/gm, (emoji) => { + return toEmoji.get(emoji) || ""; + }); +} + module.exports = { renderError, kFormatter, @@ -276,4 +290,5 @@ module.exports = { CustomError, lowercaseTrim, chunkArray, + parseEmojis, }; diff --git a/src/fetchers/repo-fetcher.js b/src/fetchers/repo-fetcher.js index 7e441084618f6..07e9a4c8b2ba4 100644 --- a/src/fetchers/repo-fetcher.js +++ b/src/fetchers/repo-fetcher.js @@ -63,7 +63,10 @@ async function fetchRepo(username, reponame) { if (!data.user.repository || data.user.repository.isPrivate) { throw new Error("User Repository Not found"); } - return data.user.repository; + return { + ...data.user.repository, + starCount: data.user.repository.stargazers.totalCount, + }; } if (isOrg) { @@ -73,7 +76,10 @@ async function fetchRepo(username, reponame) { ) { throw new Error("Organization Repository Not found"); } - return data.organization.repository; + return { + ...data.organization.repository, + starCount: data.organization.repository.stargazers.totalCount, + }; } } diff --git a/tests/fetchRepo.test.js b/tests/fetchRepo.test.js index 891cb4af296bf..277d627043ace 100644 --- a/tests/fetchRepo.test.js +++ b/tests/fetchRepo.test.js @@ -19,14 +19,14 @@ const data_repo = { const data_user = { data: { - user: { repository: data_repo }, + user: { repository: data_repo.repository }, organization: null, }, }; const data_org = { data: { user: null, - organization: { repository: data_repo }, + organization: { repository: data_repo.repository }, }, }; @@ -41,14 +41,21 @@ describe("Test fetchRepo", () => { mock.onPost("https://api.github.com/graphql").reply(200, data_user); let repo = await fetchRepo("anuraghazra", "convoychat"); - expect(repo).toStrictEqual(data_repo); + + expect(repo).toStrictEqual({ + ...data_repo.repository, + starCount: data_repo.repository.stargazers.totalCount, + }); }); it("should fetch correct org repo", async () => { mock.onPost("https://api.github.com/graphql").reply(200, data_org); let repo = await fetchRepo("anuraghazra", "convoychat"); - expect(repo).toStrictEqual(data_repo); + expect(repo).toStrictEqual({ + ...data_repo.repository, + starCount: data_repo.repository.stargazers.totalCount, + }); }); it("should throw error if user is found but repo is null", async () => { diff --git a/tests/pin.test.js b/tests/pin.test.js index 8abad44e1619c..29c19c63480fc 100644 --- a/tests/pin.test.js +++ b/tests/pin.test.js @@ -9,7 +9,9 @@ const data_repo = { repository: { username: "anuraghazra", name: "convoychat", - stargazers: { totalCount: 38000 }, + stargazers: { + totalCount: 38000, + }, description: "Help us take over the world! React + TS + GraphQL Chat App", primaryLanguage: { color: "#2b7489", @@ -51,7 +53,12 @@ describe("Test /api/pin", () => { await pin(req, res); expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toBeCalledWith(renderRepoCard(data_repo.repository)); + expect(res.send).toBeCalledWith( + renderRepoCard({ + ...data_repo.repository, + starCount: data_repo.repository.stargazers.totalCount, + }), + ); }); it("should get the query options", async () => { @@ -76,7 +83,13 @@ describe("Test /api/pin", () => { expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml"); expect(res.send).toBeCalledWith( - renderRepoCard(data_repo.repository, { ...req.query }), + renderRepoCard( + { + ...data_repo.repository, + starCount: data_repo.repository.stargazers.totalCount, + }, + { ...req.query }, + ), ); }); diff --git a/tests/renderRepoCard.test.js b/tests/renderRepoCard.test.js index a6d249821dee1..4b7060a1f88f1 100644 --- a/tests/renderRepoCard.test.js +++ b/tests/renderRepoCard.test.js @@ -9,13 +9,13 @@ const data_repo = { repository: { nameWithOwner: "anuraghazra/convoychat", name: "convoychat", - stargazers: { totalCount: 38000 }, description: "Help us take over the world! React + TS + GraphQL Chat App", primaryLanguage: { color: "#2b7489", id: "MDg6TGFuZ3VhZ2UyODc=", name: "TypeScript", }, + starCount: 38000, forkCount: 100, }, }; @@ -231,7 +231,7 @@ describe("Test renderRepoCard", () => { it("should not render star count or fork count if either of the are zero", () => { document.body.innerHTML = renderRepoCard({ ...data_repo.repository, - stargazers: { totalCount: 0 }, + starCount: 0, }); expect(queryByTestId(document.body, "stargazers")).toBeNull(); @@ -239,7 +239,7 @@ describe("Test renderRepoCard", () => { document.body.innerHTML = renderRepoCard({ ...data_repo.repository, - stargazers: { totalCount: 1 }, + starCount: 1, forkCount: 0, }); @@ -248,7 +248,7 @@ describe("Test renderRepoCard", () => { document.body.innerHTML = renderRepoCard({ ...data_repo.repository, - stargazers: { totalCount: 0 }, + starCount: 0, forkCount: 0, }); @@ -311,4 +311,15 @@ describe("Test renderRepoCard", () => { document.body.innerHTML = renderRepoCard(data_repo.repository, {}); expect(document.querySelector("rect")).toHaveAttribute("rx", "4.5"); }); + + it("should fallback to default description", () => { + document.body.innerHTML = renderRepoCard({ + ...data_repo.repository, + description: undefined, + isArchived: true, + }); + expect(document.getElementsByClassName("description")[0]).toHaveTextContent( + "No description provided", + ); + }); });