Skip to content

Commit

Permalink
refactor: refactor repo card (anuraghazra#1325)
Browse files Browse the repository at this point in the history
* refactor: refactored repo-card

* test: fix tests

* test: fixed tests

* fix: unprovided description error
  • Loading branch information
anuraghazra authored Sep 26, 2021
1 parent ec8eb0c commit 62d65ab
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 77 deletions.
2 changes: 1 addition & 1 deletion api/pin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
129 changes: 70 additions & 59 deletions src/cards/repo-card.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,79 @@
const toEmoji = require("emoji-name-map");
const {
kFormatter,
encodeHTML,
getCardColors,
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 {
Expand All @@ -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;
Expand All @@ -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("");
Expand Down Expand Up @@ -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})">
Expand Down
23 changes: 19 additions & 4 deletions src/common/utils.js
Original file line number Diff line number Diff line change
@@ -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 `
Expand Down Expand Up @@ -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[]}
*
Expand Down Expand Up @@ -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,
Expand All @@ -276,4 +290,5 @@ module.exports = {
CustomError,
lowercaseTrim,
chunkArray,
parseEmojis,
};
10 changes: 8 additions & 2 deletions src/fetchers/repo-fetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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,
};
}
}

Expand Down
15 changes: 11 additions & 4 deletions tests/fetchRepo.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
},
};

Expand All @@ -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 () => {
Expand Down
19 changes: 16 additions & 3 deletions tests/pin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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 },
),
);
});

Expand Down
Loading

0 comments on commit 62d65ab

Please sign in to comment.