Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add gist card #3064

Merged
merged 10 commits into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions api/gist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {
clampValue,
CONSTANTS,
renderError,
parseBoolean,
} from "../src/common/utils.js";
import { isLocaleAvailable } from "../src/translations.js";
import { renderGistCard } from "../src/cards/gist-card.js";
import { fetchGist } from "../src/fetchers/gist-fetcher.js";

export default async (req, res) => {
const {
id,
title_color,
icon_color,
text_color,
bg_color,
theme,
cache_seconds,
locale,
border_radius,
border_color,
show_owner,
} = req.query;

res.setHeader("Content-Type", "image/svg+xml");

if (locale && !isLocaleAvailable(locale)) {
return res.send(renderError("Something went wrong", "Language not found"));
}

try {
const gistData = await fetchGist(id);

let cacheSeconds = clampValue(
parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10),
CONSTANTS.FOUR_HOURS,
CONSTANTS.ONE_DAY,
);
cacheSeconds = process.env.CACHE_SECONDS
? parseInt(process.env.CACHE_SECONDS, 10) || cacheSeconds
: cacheSeconds;

/*
if star count & fork count is over 1k then we are kFormating the text
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 = gistData.starsCount;
const forks = gistData.forksCount;
const isBothOver1K = stars > 1000 && forks > 1000;
const isBothUnder1 = stars < 1 && forks < 1;
if (!cache_seconds && (isBothOver1K || isBothUnder1)) {
cacheSeconds = CONSTANTS.FOUR_HOURS;
}

res.setHeader(
"Cache-Control",
`max-age=${
cacheSeconds / 2
}, s-maxage=${cacheSeconds}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
);

return res.send(
renderGistCard(gistData, {
title_color,
icon_color,
text_color,
bg_color,
theme,
border_radius,
border_color,
locale: locale ? locale.toLowerCase() : null,
show_owner: parseBoolean(show_owner),
}),
);
} catch (err) {
res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses.
return res.send(renderError(err.message, err.secondaryMessage));
}
};
43 changes: 40 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,11 @@ Please visit [this link](https://give.do/fundraisers/stand-beside-the-victims-of
- [GitHub Extra Pins](#github-extra-pins)
- [Usage](#usage)
- [Demo](#demo)
- [Top Languages Card](#top-languages-card)
- [GitHub Gist Pins](#github-gist-pins)
- [Usage](#usage-1)
- [Demo](#demo-1)
- [Top Languages Card](#top-languages-card)
- [Usage](#usage-2)
- [Language stats algorithm](#language-stats-algorithm)
- [Exclude individual repositories](#exclude-individual-repositories)
- [Hide individual languages](#hide-individual-languages)
Expand All @@ -108,9 +111,9 @@ Please visit [this link](https://give.do/fundraisers/stand-beside-the-victims-of
- [Donut Vertical Chart Language Card Layout](#donut-vertical-chart-language-card-layout)
- [Pie Chart Language Card Layout](#pie-chart-language-card-layout)
- [Hide Progress Bars](#hide-progress-bars)
- [Demo](#demo-1)
- [Wakatime Stats Card](#wakatime-stats-card)
- [Demo](#demo-2)
- [Wakatime Stats Card](#wakatime-stats-card)
- [Demo](#demo-3)
- [All Demos](#all-demos)
- [Quick Tip (Align The Cards)](#quick-tip-align-the-cards)
- [Deploy on your own](#deploy-on-your-own)
Expand Down Expand Up @@ -328,6 +331,10 @@ You can provide multiple comma-separated values in the bg\_color option to rende

* `show_owner` - Shows the repo's owner name *(boolean)*. Default: `false`.

#### Gist Card Exclusive Options

* `show_owner` - Shows the gist's owner name *(boolean)*. Default: `false`.

#### Language Card Exclusive Options

* `hide` - Hides the languages specified from the card *(Comma-separated values)*. Default: `[] (blank array)`.
Expand Down Expand Up @@ -384,6 +391,28 @@ Use [show\_owner](#repo-card-exclusive-options) query option to include the repo

![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra\&repo=github-readme-stats\&show_owner=true)

# GitHub Gist Pins

GitHub gist pins allow you to pin gists in your GitHub profile using a GitHub readme profile.

### Usage

Copy-paste this code into your readme and change the links.

Endpoint: `api/gist?id=bbfce31e0217a3689c8d961a356cb10d`

```md
[![Gist Card](https://github-readme-stats.vercel.app/api/gist?id=bbfce31e0217a3689c8d961a356cb10d)](https://gist.github.com/Yizack/bbfce31e0217a3689c8d961a356cb10d/)
```

### Demo

![Gist Card](https://github-readme-stats.vercel.app/api/gist?id=bbfce31e0217a3689c8d961a356cb10d)

Use [show\_owner](#gist-card-exclusive-options) query option to include the gist's owner username

![Gist Card](https://github-readme-stats.vercel.app/api/gist?id=bbfce31e0217a3689c8d961a356cb10d\&show_owner=true)

# Top Languages Card

The top languages card shows a GitHub user's most frequently used languages.
Expand Down Expand Up @@ -592,6 +621,14 @@ Choose from any of the [default themes](#themes)

![Customized Card](https://github-readme-stats.vercel.app/api/pin?username=anuraghazra\&repo=github-readme-stats\&title_color=fff\&icon_color=f9f9f9\&text_color=9f9f9f\&bg_color=151515)

* Gist card

![Gist Card](https://github-readme-stats.vercel.app/api/gist?id=bbfce31e0217a3689c8d961a356cb10d)

* Customizing gist card

![Gist Card](https://github-readme-stats.vercel.app/api/gist?id=bbfce31e0217a3689c8d961a356cb10d&theme=calm)

* Top languages

![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)
Expand Down
180 changes: 180 additions & 0 deletions src/cards/gist-card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// @ts-check

import {
getCardColors,
parseEmojis,
wrapTextMultiline,
encodeHTML,
kFormatter,
measureText,
flexLayout,
} from "../common/utils.js";
import Card from "../common/Card.js";
import { icons } from "../common/icons.js";

/** Import language colors.
*
* @description Here we use the workaround found in
* https://stackoverflow.com/questions/66726365/how-should-i-import-json-in-node
* since vercel is using v16.14.0 which does not yet support json imports without the
* --experimental-json-modules flag.
*/
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const languageColors = require("../common/languageColors.json"); // now works

const ICON_SIZE = 16;
const CARD_DEFAULT_WIDTH = 400;
const HEADER_MAX_LENGTH = 35;

/**
* Creates a node to display the primary programming language of the gist.
*
* @param {string} langName Language name.
* @param {string} langColor Language color.
* @returns {string} Language display SVG object.
*/
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>
`;
};

/**
* Creates an icon with label to display gist stats like forks, stars, etc.
*
* @param {string} icon The icon to display.
* @param {number|string} label The label to display.
* @param {string} testid The testid to assign to the label.
* @returns {string} Icon with label SVG object.
*/
const iconWithLabel = (icon, label, testid) => {
if (typeof label === "number" && 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("");
};

/**
* @typedef {import('./types').GistCardOptions} GistCardOptions Gist card options.
* @typedef {import('../fetchers/types').GistData} GistData Gist data.
*/

/**
* Render gist card.
*
* @param {GistData} gistData Gist data.
* @param {Partial<GistCardOptions>} options Gist card options.
* @returns {string} Gist card.
*/
const renderGistCard = (gistData, options = {}) => {
const { name, nameWithOwner, description, language, starsCount, forksCount } =
gistData;
const {
title_color,
icon_color,
text_color,
bg_color,
theme,
border_radius,
border_color,
show_owner = false,
} = options;

// returns theme based colors with proper overrides and defaults
const { titleColor, textColor, iconColor, bgColor, borderColor } =
getCardColors({
title_color,
icon_color,
text_color,
bg_color,
border_color,
theme,
});

const lineWidth = 59;
const linesLimit = 10;
const desc = parseEmojis(description || "No description provided");
const multiLineDescription = wrapTextMultiline(desc, lineWidth, linesLimit);
const descriptionLines = multiLineDescription.length;
const descriptionSvg = multiLineDescription
.map((line) => `<tspan dy="1.2em" x="25">${encodeHTML(line)}</tspan>`)
.join("");

const lineHeight = descriptionLines > 3 ? 12 : 10;
const height =
(descriptionLines > 1 ? 120 : 110) + descriptionLines * lineHeight;

const totalStars = kFormatter(starsCount);
const totalForks = kFormatter(forksCount);
const svgStars = iconWithLabel(icons.star, totalStars, "starsCount");
const svgForks = iconWithLabel(icons.fork, totalForks, "forksCount");

const languageName = language || "Unspecified";
const languageColor = languageColors[languageName] || "#858585";

const svgLanguage = createLanguageNode(languageName, languageColor);

const starAndForkCount = flexLayout({
items: [svgLanguage, svgStars, svgForks],
sizes: [
measureText(languageName, 12),
ICON_SIZE + measureText(`${totalStars}`, 12),
ICON_SIZE + measureText(`${totalForks}`, 12),
],
gap: 25,
}).join("");

const header = show_owner ? nameWithOwner : name;

const card = new Card({
defaultTitle:
header.length > HEADER_MAX_LENGTH
? `${header.slice(0, HEADER_MAX_LENGTH)}...`
: header,
titlePrefixIcon: icons.gist,
width: CARD_DEFAULT_WIDTH,
height,
border_radius,
colors: {
titleColor,
textColor,
iconColor,
bgColor,
borderColor,
},
});

card.setCSS(`
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
.icon { fill: ${iconColor} }
`);

return card.render(`
<text class="description" x="25" y="-5">
${descriptionSvg}
</text>

<g transform="translate(30, ${height - 75})">
${starAndForkCount}
</g>
`);
};

export { renderGistCard, HEADER_MAX_LENGTH };
export default renderGistCard;
4 changes: 4 additions & 0 deletions src/cards/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,7 @@ type WakaTimeOptions = CommonOptions & {
layout: "compact" | "normal";
langs_count: number;
};

export type GistCardOptions = CommonOptions & {
show_owner: boolean;
};
1 change: 1 addition & 0 deletions src/common/icons.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading