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

Creates a new Streaks Card #3338

Closed
wants to merge 13 commits into from
2 changes: 2 additions & 0 deletions api/gist.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default async (req, res) => {
border_color,
show_owner,
hide_border,
card_width,
} = req.query;

res.setHeader("Content-Type", "image/svg+xml");
Expand Down Expand Up @@ -74,6 +75,7 @@ export default async (req, res) => {
locale: locale ? locale.toLowerCase() : null,
show_owner: parseBoolean(show_owner),
hide_border: parseBoolean(hide_border),
card_width: parseInt(card_width),
}),
);
} catch (err) {
Expand Down
87 changes: 87 additions & 0 deletions api/streak.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import renderStreaksCard from "../src/cards/streak-card.js";
import { blacklist } from "../src/common/blacklist.js";
import {
clampValue,
CONSTANTS,
parseArray,
parseBoolean,
renderError,
} from "../src/common/utils.js";

import { isLocaleAvailable } from "../src/translations.js";
import { streakFetcher } from "../src/fetchers/streak-fetcher.js";
import axios from "axios";
yaten2302 marked this conversation as resolved.
Show resolved Hide resolved
export default async (req, res) => {
const {
username,
hide,
hide_border,
yaten2302 marked this conversation as resolved.
Show resolved Hide resolved
card_width,
line_height,
title_color,
text_color,
text_bold,
bg_color,
theme,
cache_seconds,
custom_title,
locale,
disable_animations,
border_radius,
number_format,
border_color,
} = req.query;
res.setHeader("Content-Type", "image/svg+xml");

if (blacklist.includes(username)) {
return res.send(renderError("Something went wrong"));
}

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

try {
const streaks = await streakFetcher(username);

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;

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

return res.send(
renderStreaksCard(streaks, {
hide: parseArray(hide),
hide_border: parseBoolean(hide_border),
card_width: parseInt(card_width, 10),
line_height,
title_color,
text_color,
text_bold: parseBoolean(text_bold),
bg_color,
theme,
custom_title,
border_radius,
border_color,
number_format,
locale: locale ? locale : null,
disable_animations: parseBoolean(disable_animations),
}),
);

} catch (err) {
res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses.
yaten2302 marked this conversation as resolved.
Show resolved Hide resolved
return res.send(renderError(err.message, err.secondaryMessage));
}
};
21 changes: 21 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ Please visit [this link](https://give.do/fundraisers/stand-beside-the-victims-of
- [Demo](#demo-2)
- [Wakatime Stats Card](#wakatime-stats-card)
- [Demo](#demo-3)
- [GitHub Streaks Card](#github-streaks-card)
- [Usage](#usage-3)
- [Hide individual elements from the streaks card](#hide-individual-elements-from-the-streaks-card)
- [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 @@ -624,6 +627,24 @@ Change the `?username=` value to your [Wakatime](https://wakatime.com) username.

***
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
***


# GitHub Streaks Card

GitHub Streaks Card allows you to show your streak of your contributions on GitHub(like total contributions, weekly and longest streak on daily and weekly basis).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
GitHub Streaks Card allows you to show your streak of your contributions on GitHub(like total contributions, weekly and longest streak on daily and weekly basis).
The GitHub streaks card highlights your GitHub journey, summarizing total contributions, weekly activity, and your best daily and weekly streaks.


### Usage

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

Endpoint: `api/streak/?username=YOUR_USERNAME`

```md
![Streaks Card](https://github-readme-stats.vercel.app/api/streak/?username=YOUR_USERNAME)
```

### Hide individual elements from the streaks card

To hide individual elements form the card, use - `&hide=params`, where `params` = `totalContributions`, `weeklyLongest`, `weeklyCurrent`, `dailyLongest`, `dailyCurrent`

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
***

# All Demos

* Default
Expand Down
195 changes: 195 additions & 0 deletions src/cards/streak-card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import Card from "../common/Card.js";
import I18n from "../common/I18n.js";
import { flexLayout, getCardColors, kFormatter } from "../common/utils.js";
import { getStyles } from "../getStyles.js";
import { streakCardLocales } from "../translations.js";

const CARD_MIN_WIDTH = 300;
const CARD_DEFAULT_WIDTH = 300;

const createTextNode = ({
yaten2302 marked this conversation as resolved.
Show resolved Hide resolved
value,
label,
id,
index,
shiftValuePos,
bold,
number_format,
}) => {
const kValue = number_format === "long" ? value : kFormatter(value);
const staggerDelay = (index + 3) * 150;

return `
<g class="stagger" style="animation-delay: ${staggerDelay}ms" transform="translate(25,0)">
<text class="stat ${bold ? "bold" : "not_bold"}" y="12.5">${label}:</text>
<text class="stat ${bold ? "bold" : "not_bold"}" x="${
shiftValuePos + 120
}" y="12.5" data-testid="${id}">${kValue}</text>
</g>
`;
};

const renderStreaksCard = (streaksData, options = {}) => {
yaten2302 marked this conversation as resolved.
Show resolved Hide resolved
const {
user_name,
yaten2302 marked this conversation as resolved.
Show resolved Hide resolved
longest_streak_weekly,
current_streak_weekly,
longest_streak_daily,
current_streak_daily,
total_contributions,
} = streaksData;
const {
hide = [],
hide_border = false,
card_width,
line_height = 25,
title_color,
text_color,
text_bold = true,
bg_color,
theme = "default",
custom_title,
border_radius,
border_color,
number_format = "short",
locale,
disable_animations = false,
} = options;

const lheight = String(parseInt(line_height), 10);

const { titleColor, textColor, bgColor, borderColor } = getCardColors({
title_color,
text_color,
bg_color,
border_color,
theme,
});

const apostrophe = ["x", "s"].includes(user_name.slice(-1)) ? "" : "s";

const i18n = new I18n({
locale,
translations: streakCardLocales({ user_name, apostrophe }),
});

const STREAKS = {};

STREAKS.totalContributions = {
label: i18n.t("streakcard.totalcontributions"),
value: total_contributions,
id: "total contributions",
};

STREAKS.weeklyLongest = {
label: i18n.t("streakcard.longest"),
value: longest_streak_weekly + " weeks",
id: "weekly longest streak",
};

STREAKS.weeklyCurrent = {
label: i18n.t("streakcard.current"),
value: current_streak_weekly + " weeks",
id: "weekly current streak",
};

STREAKS.dailyLongest = {
label: i18n.t("streakcard.longest"),
value: longest_streak_daily + " days",
id: "daily longest streak",
};

STREAKS.dailyCurrent = {
label: i18n.t("streakcard.current"),
value: current_streak_daily + " days",
id: "daily longest streak",
};
const longLocales = [
"cn",
"es",
"fr",
"pt-br",
"ru",
"uk-ua",
"id",
"ml",
"my",
"pl",
"de",
"nl",
"zh-tw",
"uz",
];
const isLongLocale = locale ? longLocales.includes(locale) : false;

const streakItems = Object.keys(STREAKS)
.filter((key) => !hide.includes(key))
.map((key, index) =>
// create the text nodes, and pass index so that we can calculate the line spacing
createTextNode({
label: STREAKS[key].label,
value: STREAKS[key].value,
id: STREAKS[key].id,
index,
shiftValuePos: 79.01 + (isLongLocale ? 50 : 0),
bold: text_bold,
number_format,
}),
);

const cssStyles = getStyles({
titleColor,
textColor,
});

let height = 45 + (streakItems.length + 1) * lheight;

const minCardWidth = CARD_MIN_WIDTH;
const defaultCardWidth = CARD_DEFAULT_WIDTH;
let width = card_width
? isNaN(card_width)
? defaultCardWidth
: card_width
: defaultCardWidth;
if (width < minCardWidth) {
width = minCardWidth;
}

const card = new Card({
customTitle: custom_title,
defaultTitle: streakItems.length
? i18n.t("streakcard.title")
: i18n.t("streakcard.title"),
width,
height,
border_radius,
colors: {
titleColor,
textColor,
bgColor,
borderColor,
},
});

card.setHideBorder(hide_border);
card.setCSS(cssStyles);

if (disable_animations) card.disableAnimations();

card.setAccessibilityLabel({
title: `${card.title}`,
});

return card.render(`
<svg x="0" y="0">
${flexLayout({
items: streakItems,
gap: Number(lheight),
direction: "column",
}).join("")}
</svg>
`);
};

export { renderStreaksCard };
export default renderStreaksCard;
18 changes: 14 additions & 4 deletions src/common/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,23 @@ const iconWithLabel = (icon, label, testid, iconSize) => {
/**
* Retrieves num with suffix k(thousands) precise to 1 decimal if greater than 999.
*
* @param {number} num The number to format.
* @returns {string|number} The formatted number.
* num The number to format.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are the typings removed 🤔 ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess, I removed those typings by mistake, will add those again 👍

* The formatted number.
*/
const kFormatter = (num) => {
return Math.abs(num) > 999
/* return Math.abs(num) > 999
? Math.sign(num) * parseFloat((Math.abs(num) / 1000).toFixed(1)) + "k"
: Math.sign(num) * Math.abs(num);
: Math.sign(num) * Math.abs(num); */

if (typeof num == "number") {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this need to be changed? This function should only accept numbers. In its current form strings should be converted before they enter the function.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this because while rendering the streaks card, it requires some additional information in the value (like - days or weeks)

image
If this is reverted to the original code, then it will show NaN in the value instead of 2 weeks or something.
This change will not affect the other cards. But, if you want, then I can make a separate function in the utils.js specifically for the Streaks card?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. You should handle this problem outside the kFormatter function. Maybe only use the kformatter function when the value is a number:

const kValue = number_format === "long" ? value : kFormatter(value);

return Math.abs(num) > 999
? Math.sign(num) * parseFloat((Math.abs(num) / 1000).toFixed(1)) + "k"
: Math.sign(num) * Math.abs(num);
}

if (typeof num == "string") {
return num;
}
};

/**
Expand Down
Loading