refactor: refactor repo card (#1325)

* refactor: refactored repo-card

* test: fix tests

* test: fixed tests

* fix: unprovided description error
This commit is contained in:
Anurag Hazra 2021-09-26 21:02:27 +05:30 committed by GitHub
parent ec8eb0c893
commit 62d65ab483
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 140 additions and 77 deletions

View File

@ -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;

View File

@ -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) => `
<g data-testid="badge" class="badge" transform="translate(320, -18)">
<rect stroke="${textColor}" stroke-width="1" width="70" height="20" x="-12" y="-14" ry="10" rx="10"></rect>
<text
x="23" y="-5"
alignment-baseline="central"
dominant-baseline="central"
text-anchor="middle"
fill="${textColor}"
>
${label}
</text>
</g>
`;
/**
* @param {string} langName
* @param {string} langColor
* @returns {string}
*/
const createLanguageNode = (langName, langColor) => {
return `
<g data-testid="primary-lang">
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="${langColor}" />
<text data-testid="lang-name" class="gray" x="15">${langName}</text>
</g>
`;
};
const ICON_SIZE = 16;
const iconWithLabel = (icon, label, testid) => {
if (label <= 0) return "";
const iconSvg = `
<svg
class="icon"
y="-12"
viewBox="0 0 16 16"
version="1.1"
width="${ICON_SIZE}"
height="${ICON_SIZE}"
>
${icon}
</svg>
`;
const text = `<text data-testid="${testid}" class="gray">${label}</text>`;
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) => `<tspan dy="1.2em" x="25">${encodeHTML(line)}</tspan>`)
.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) => `
<g data-testid="badge" class="badge" transform="translate(320, -18)">
<rect stroke="${textColor}" stroke-width="1" width="70" height="20" x="-12" y="-14" ry="10" rx="10"></rect>
<text
x="23" y="-5"
alignment-baseline="central"
dominant-baseline="central"
text-anchor="middle"
fill="${textColor}"
>
${label}
</text>
</g>
`;
const svgLanguage = primaryLanguage
? `
<g data-testid="primary-lang">
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="${langColor}" />
<text data-testid="lang-name" class="gray" x="15">${langName}</text>
</g>
`
? createLanguageNode(langName, langColor)
: "";
const iconSize = 16;
const iconWithLabel = (icon, label, testid) => {
const iconSvg = `
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="${iconSize}" height="${iconSize}">
${icon}
</svg>
`;
const text = `<text data-testid="${testid}" class="gray">${label}</text>`;
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)
: ""
}
<text class="description" x="25" y="-5">
${multiLineDescription
.map((line) => `<tspan dy="1.2em" x="25">${encodeHTML(line)}</tspan>`)
.join("")}
${descriptionSvg}
</text>
<g transform="translate(30, ${height - 75})">

View File

@ -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,
};

View File

@ -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,
};
}
}

View File

@ -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 () => {

View File

@ -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 },
),
);
});

View File

@ -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",
);
});
});