Compare commits

...
Author SHA1 Message Date
anuraghazra fdf445d734 feat: added ability to set custom cache
Test / build (push) Canceled after 0s
2020-07-20 21:43:51 +05:30
Sreetam Dasandanuraghazra dd2c7ed278 fix: proper apostrophe in stats card's header
* fix: remove 's' after apostrophe if name ends with 's'

* fix: respect GITHUB_TOKEN env var before PAT_s

* revert: verbose console log for retries

* Revert "fix: respect GITHUB_TOKEN env var before PAT_s"

This reverts commit 2becbe595c2df4580aa81d690482187b1e6ba512.

* test: added test for title apostrophe

Co-authored-by: anuraghazra <hazru.anurag@gmail.com>
2020-07-20 00:02:07 +05:30
Anurag Hazra 9a7ed8f192 docs: added themes screenshot 2020-07-19 21:35:57 +05:30
Anurag Hazra 71c63abe3a Merge pull request #106 from hi120ki/fix-style-collapses-on-stats-card
fix: text collapses on stats card with proper macos font fallback
2020-07-19 21:25:13 +05:30
Hi120ki 30da0119bf fix style collapses on stats card by css font config 2020-07-20 00:24:54 +09:00
Anurag Hazra 2c26329e13 feat: added inbuilt themes (#105)
* feat: added inbuilt themes

* docs: added theming docs

* docs: update docs
2020-07-19 20:34:41 +05:30
Anurag Hazra b4a9bd4468 Merge pull request #102 from JalenChuh/master
docs(translation): add Chinese translation
2020-07-19 18:08:58 +05:30
Anurag Hazra f5cd85f11f Merge pull request #103 from colmugx/patch-1
docs: fix readme hide options
2020-07-19 14:16:06 +05:30
ColMugX c436ed1121 docs: README.md 2020-07-19 14:33:48 +08:00
Jalen Chuh f6c901b938 📝 add Chinese translation 2020-07-19 12:00:32 +08:00
Anurag Hazra e9dc35318b Merge pull request #100 from anuraghazra/refactor-flex
refactor: added FlexLayout for flex layouts
2020-07-18 23:14:47 +05:30
anuraghazra 253eb39b19 refactor: added FlexLayout for flex layouts
Test / build (push) Canceled after 0s
2020-07-18 22:44:27 +05:30
Omid Nikrah 06f0021660 chore: Hide star and fork stats in repo card if count of equals to zero (#97)
* add show_icons option to show/hide star and fork icon in repo cards

* resolve conflict

* resolve conflict

* hide star and fork stats in repo card if count of equals to zero

* HOTFIX

* HOTFIX
2020-07-18 22:20:04 +05:30
Anurag Hazra f27daeb931 Merge pull request #99 from anuraghazra/feat/archive-badge
feat: show archive badge if repo is archived
2020-07-18 20:33:19 +05:30
13 changed files with 789 additions and 99 deletions
+17 -2
View File
@@ -1,5 +1,10 @@
require("dotenv").config();
const { renderError, parseBoolean } = require("../src/utils");
const {
renderError,
parseBoolean,
clampValue,
CONSTANTS,
} = require("../src/utils");
const fetchStats = require("../src/fetchStats");
const renderStatsCard = require("../src/renderStatsCard");
@@ -16,10 +21,11 @@ module.exports = async (req, res) => {
icon_color,
text_color,
bg_color,
theme,
cache_seconds,
} = req.query;
let stats;
res.setHeader("Cache-Control", "public, max-age=1800");
res.setHeader("Content-Type", "image/svg+xml");
try {
@@ -28,6 +34,14 @@ module.exports = async (req, res) => {
return res.send(renderError(err.message));
}
const cacheSeconds = clampValue(
parseInt(cache_seconds || CONSTANTS.THIRTY_MINUTES, 10),
CONSTANTS.THIRTY_MINUTES,
CONSTANTS.ONE_DAY
);
res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`);
res.send(
renderStatsCard(stats, {
hide: JSON.parse(hide || "[]"),
@@ -40,6 +54,7 @@ module.exports = async (req, res) => {
icon_color,
text_color,
bg_color,
theme,
})
);
};
+30 -2
View File
@@ -1,5 +1,10 @@
require("dotenv").config();
const { renderError, parseBoolean } = require("../src/utils");
const {
renderError,
parseBoolean,
clampValue,
CONSTANTS,
} = require("../src/utils");
const fetchRepo = require("../src/fetchRepo");
const renderRepoCard = require("../src/renderRepoCard");
@@ -11,12 +16,13 @@ module.exports = async (req, res) => {
icon_color,
text_color,
bg_color,
theme,
show_owner,
cache_seconds,
} = req.query;
let repoData;
res.setHeader("Cache-Control", "public, max-age=1800");
res.setHeader("Content-Type", "image/svg+xml");
try {
@@ -26,12 +32,34 @@ module.exports = async (req, res) => {
return res.send(renderError(err.message));
}
let cacheSeconds = clampValue(
parseInt(cache_seconds || CONSTANTS.THIRTY_MINUTES, 10),
CONSTANTS.THIRTY_MINUTES,
CONSTANTS.ONE_DAY
);
/*
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 = repoData.stargazers.totalCount;
const forks = repoData.forkCount;
const isBothOver1K = stars > 1000 && forks > 1000;
const isBothUnder1 = stars < 1 && forks < 1;
if (!cache_seconds && (isBothOver1K || isBothUnder1)) {
cacheSeconds = CONSTANTS.TWO_HOURS;
}
res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`);
res.send(
renderRepoCard(repoData, {
title_color,
icon_color,
text_color,
bg_color,
theme,
show_owner: parseBoolean(show_owner),
})
);
+33 -6
View File
@@ -23,6 +23,9 @@
·
<a href="https://github.com/anuraghazra/github-readme-stats/issues">Request Feature</a>
</p>
<p align="center">
<a href="readme_cn.md">简体中文</a>
</p>
</p>
<p align="center">Loved the project? Please consider <a href="https://www.paypal.me/anuraghazra">donating</a> to help it improve!
@@ -30,6 +33,7 @@
- [GitHub Stats Card](#github-stats-card)
- [GitHub Extra Pins](#github-extra-pins)
- [Themes](#themes)
- [Customization](#customization)
- [Deploy Yourself](#deploy-on-your-own-vercel-instance)
@@ -49,7 +53,7 @@ _Note: Ranks are calculated based on user's stats, see [src/calculateRank.js](./
To hide any specific stats, you can pass a query parameter `?hide=` with an array of items you wanna hide.
> Options: `&hide=["stars","prs","issues","contribs"]`
> Options: `&hide=["stars","commits","prs","issues","contribs"]`
```md
![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=["contribs","prs"])
@@ -63,6 +67,24 @@ To enable icons, you can pass `show_icons=true` in the query param, like so:
![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true)
```
### Themes
With inbuilt themes you can customize the look of the card without doing any [manual customization](#customization).
Use `?theme=THEME_NAME` parameter like so :-
```md
![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=radical)
```
#### All inbuilt themes :-
dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula
<img src="https://res.cloudinary.com/anuraghazra/image/upload/v1595174536/grs-themes_l4ynja.png" alt="Github Readme Stat Themes" width="600px"/>
Check out more themes at [theme config file](./themes/index.js) & **you can also contribute new themes** if you like :D
### Customization
You can customize the appearance of your `Stats Card` or `Repo Card` however you want with URL params.
@@ -81,12 +103,9 @@ Customization Options:
| hide_border | boolean | hides the stats card border | false | N/A |
| show_owner | boolean | shows owner name in repo card | N/A | false |
| show_icons | boolean | shows icons | false | N/A |
| theme | string | sets inbuilt theme | 'default' | 'default_repocard' |
- You can also customize the cards to be compatible with dark mode
```md
![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&title_color=fff&icon_color=79ff97&text_color=9f9f9f&bg_color=151515)
```
---
### Demo
@@ -102,6 +121,12 @@ Customization Options:
![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=["issues"]&show_icons=true)
- Themes
Choose from any of the [default themes](#themes)
![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=radical)
- Customizing stats card
![Anurag's github stats](https://github-readme-stats.vercel.app/api/?username=anuraghazra&show_icons=true&title_color=fff&icon_color=79ff97&text_color=9f9f9f&bg_color=151515)
@@ -110,6 +135,8 @@ Customization Options:
![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)
---
# GitHub Extra Pins
GitHub extra pins allow you to pin more than 6 repositories in your profile using a GitHub readme profile.
+194
View File
@@ -0,0 +1,194 @@
<p align="center">
<img width="100px" src="https://res.cloudinary.com/anuraghazra/image/upload/v1594908242/logo_ccswme.svg" align="center" alt="Github Readme Stats" />
<h2 align="center">GitHub Readme Stats</h2>
<p align="center">在你的 README 中 获取动态生成的GitHub统计信息!</p>
</p>
<p align="center">
<a href="https://github.com/anuraghazra/github-readme-stats/actions">
<img alt="Tests Passing" src="https://github.com/anuraghazra/github-readme-stats/workflows/Test/badge.svg" />
</a>
<a href="https://github.com/anuraghazra/github-readme-stats/issues">
<img alt="Issues" src="https://img.shields.io/github/issues/anuraghazra/github-readme-stats?color=0088ff" />
</a>
<a href="https://github.com/anuraghazra/github-readme-stats/pulls">
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/anuraghazra/github-readme-stats?color=0088ff" />
</a>
</p>
<p align="center">
<a href="#demo">查看 Demo</a>
·
<a href="https://github.com/anuraghazra/github-readme-stats/issues">报告 bug</a>
·
<a href="https://github.com/anuraghazra/github-readme-stats/issues">请求增加功能</a>
</p>
</p>
<p align="center">喜欢这个项目? 请考虑<a href="https://www.paypal.me/anuraghazra">捐赠</a>来帮助它完善!
# 特性
- [GitHub 统计卡片](#GitHub-统计卡片)
- [GitHub Extra Pins](#github-extra-pins)
- [自定义](#自定义)
- [自己部署](#自己部署)
# GitHub 统计卡片
将这复制到你的 markdown 中,就这样,非常简单!
更改 `?username=` 的值为你的用户名。
```md
[![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats)
```
_Note: 排名是基于用户的统计信息计算得出的,详见 [src/calculateRank.js](./src/calculateRank.js)_
### 隐藏个人统计信息
想要隐藏某个统计信息, 你可以传递参数 `?hide=`,并将你想要隐藏的项目用数组进行传递。
> 选项: `&hide=["stars","prs","issues","contribs"]`
```md
![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=["contribs","prs"])
```
### 显示图标
想要显示图标, 你可以传递 `show_icons=true`,像这样:
```md
![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true)
```
### 自定义
你可以为你的 `Stats Card` 或 `Repo Card` 自定义样式,however you want with URL params.
自定义选项:
| Option | type | description | Stats Card (default) | Repo Card (default) |
| ----------- | --------- | ------------------------------------ | -------------------- | ------------------- |
| title_color | hex color | title color | 2f80ed | 2f80ed |
| text_color | hex color | body color | 333 | 333 |
| icon_color | hex color | icon color | 4c71f2 | 586069 |
| bg_color | hex color | card bg color | FFFEFE | FFFEFE |
| line_height | number | control the line-height between text | 30 | N/A |
| hide_rank | boolean | hides the ranking | false | N/A |
| hide_title | boolean | hides the stats title | false | N/A |
| hide_border | boolean | hides the stats card border | false | N/A |
| show_owner | boolean | shows owner name in repo card | N/A | false |
| show_icons | boolean | shows icons | false | N/A |
- 你也可以通过自定义来兼容暗黑模式
```md
![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&title_color=fff&icon_color=79ff97&text_color=9f9f9f&bg_color=151515)
```
### Demo
- 默认
![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra)
- 隐藏某个项目
![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=["contribs","issues"])
- 显示图标
![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=["issues"]&show_icons=true)
- 自定义添加卡片
![Anurag's github stats](https://github-readme-stats.vercel.app/api/?username=anuraghazra&show_icons=true&title_color=fff&icon_color=79ff97&text_color=9f9f9f&bg_color=151515)
- 自定义仓库卡片
![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)
# GitHub Extra Pins
GitHub extra pins allow you to pin more than 6 repositories in your profile using a GitHub readme profile.
Yey! You are no longer limited to 6 pinned repositories.
### Usage
Copy-paste this code into your readme and change the links.
Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`
```md
[![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](https://github.com/anuraghazra/github-readme-stats)
```
### Demo
[![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](https://github.com/anuraghazra/github-readme-stats)
Use [show_owner](#自定义) variable to include the repo's owner username
[![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&show_owner=true)](https://github.com/anuraghazra/github-readme-stats)
### 提示 (Align The Repo Cards)
你通常无法将图片靠边显示,为此,您可以使用以下方法:
```md
<a href="https://github.com/anuraghazra/github-readme-stats">
<img align="left" src="https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats" />
</a>
<a href="https://github.com/anuraghazra/convoychat">
<img align="left" src="https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=convoychat" />
</a>
```
## 自己部署
自从 GitHub 的 API 每个小时只允许 5 千的请求,我都 `https://github-readme-stats.vercel.app/api` 很有可能会触发限制
如果你将其托管在自己的 Vercel 服务商,那么你就不用担心任何事了。点击 deploy 按钮来开始
注意: 从 [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) 开始,我们应该能够处理超过 5千 的请求,并且不会出现宕机问题 :D
[![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats)
<details>
<summary>Guide on setting up Vercel</summary>
1. 前往 [vercel.com](https://vercel.com/)
1. 点击 `Log in`
![](https://files.catbox.moe/tct1wg.png)
1. 点击 `Continue with GitHub` 通过 GitHub 进行登录
![](https://files.catbox.moe/btd78j.jpeg)
1. 登录 GitHub 并允许访问所有存储库(如果系统提示)
1. Fork 这个仓库
1. 返回到你的 [Vercel dashboard](https://vercel.com/dashboard)
1. 选择 `Import Project`
![](https://files.catbox.moe/qckos0.png)
1. 选择 `Import Git Repository`
![](https://files.catbox.moe/pqub9q.png)
1. 选择 root 并将所有内容保持不变,并且只需添加名为 PAT_1 的环境变量(如图所示),其中将包含一个个人访问令牌(PAT),你可以在[这里](https://github.com/settings/tokens/new)轻松创建(保留默认,并且只需要命名下,名字随便)
![](https://files.catbox.moe/caem5b.png)
1. 点击 deploy, 这就完成了,查看你的域名就可使用 API 了!
</details>
## :sparkling_heart: 支持这个项目
我几乎将我所能的进行开源,并且我试着回复每个在使用这些项目时需要帮助的人。很明显,这需要时间,但你可以免费享受此服务。
然而, 如果你正在使用这个项目并且感觉挺开心或者只是想要支持我继续开发,这里有一些方法:
- 在你的 README 中使用 github-readme-stats 时,链接指向会这里 :D
- Star 并 分享这个项目 :rocket:
- [![paypal.me/anuraghazra](https://ionicabizau.github.io/badges/paypal.svg)](https://www.paypal.me/anuraghazra) - 你可以通过 PayPal 一次性捐款. 我可能会买一杯~~咖啡~~茶. :tea:
谢谢! :heart:
---
欢迎贡献! <3
用 :heart: 发电,用 JavaScript 制作。
+1 -1
View File
@@ -52,7 +52,7 @@ const getStyles = ({
animation: fadeIn 0.8s ease-in-out forwards;
}
.stat {
font: 600 14px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor};
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
}
.stagger {
opacity: 0;
+51 -22
View File
@@ -1,4 +1,9 @@
const { kFormatter, encodeHTML, fallbackColor } = require("../src/utils");
const {
kFormatter,
encodeHTML,
getCardColors,
FlexLayout,
} = require("../src/utils");
const icons = require("./icons");
const renderRepoCard = (repo, options = {}) => {
@@ -11,7 +16,14 @@ const renderRepoCard = (repo, options = {}) => {
isArchived,
forkCount,
} = repo;
const { title_color, icon_color, text_color, bg_color, show_owner } = options;
const {
title_color,
icon_color,
text_color,
bg_color,
show_owner,
theme = "default_repocard",
} = options;
const header = show_owner ? nameWithOwner : name;
const langName = primaryLanguage ? primaryLanguage.name : "Unspecified";
@@ -25,10 +37,14 @@ const renderRepoCard = (repo, options = {}) => {
desc = `${description.slice(0, 55)}..`;
}
const titleColor = fallbackColor(title_color, "#2f80ed");
const iconColor = fallbackColor(icon_color, "#586069");
const textColor = fallbackColor(text_color, "#333");
const bgColor = fallbackColor(bg_color, "#FFFEFE");
// returns theme based colors with proper overrides and defaults
const { titleColor, textColor, iconColor, bgColor } = getCardColors({
title_color,
icon_color,
text_color,
bg_color,
theme,
});
const totalStars = kFormatter(stargazers.totalCount);
const totalForks = kFormatter(forkCount);
@@ -42,6 +58,31 @@ const renderRepoCard = (repo, options = {}) => {
`
: "";
const svgLanguage = `
<g transform="translate(30, 100)">
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="${langColor}" />
<text data-testid="lang" class="gray" x="15">${langName}</text>
</g>
`;
const svgStars =
stargazers.totalCount > 0 &&
`
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
${icons.star}
</svg>
<text data-testid="stargazers" class="gray" x="25">${totalStars}</text>
`;
const svgForks =
forkCount > 0 &&
`
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
${icons.fork}
</svg>
<text data-testid="forkcount" class="gray" x="25">${totalForks}</text>
`;
return `
<svg version="1.1" width="400" height="${height}" viewBox="0 0 400 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
@@ -52,7 +93,7 @@ const renderRepoCard = (repo, options = {}) => {
.archive-badge { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; }
.archive-badge rect { opacity: 0.2 }
</style>
<rect data-testid="card-border" x="0.5" y="0.5" width="399" height="99%" rx="4.5" fill="${bgColor}" stroke="#E4E2E2"/>
<rect data-testid="card-bg" x="0.5" y="0.5" width="399" height="99%" rx="4.5" fill="${bgColor}" stroke="#E4E2E2"/>
<svg class="icon" x="25" y="25" viewBox="0 0 16 16" version="1.1" width="16" height="16">
${icons.contribs}
</svg>
@@ -62,24 +103,12 @@ const renderRepoCard = (repo, options = {}) => {
<text x="50" y="38" class="header">${header}</text>
<text class="description" x="25" y="70">${encodeHTML(desc)}</text>
<g transform="translate(30, 100)">
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="${langColor}" />
<text data-testid="lang" class="gray" x="15">${langName}</text>
</g>
${svgLanguage}
<g transform="translate(${155 - shiftText}, 100)">
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
${icons.star}
</svg>
<text data-testid="stargazers" class="gray" x="25">${totalStars}</text>
${FlexLayout({ items: [svgStars, svgForks], gap: 65 }).join("")}
</g>
<g transform="translate(${220 - shiftText}, 100)">
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
${icons.fork}
</svg>
<text data-testid="forkcount" class="gray" x="25">${totalForks}</text>
</g>
</svg>
`;
};
+23 -24
View File
@@ -1,22 +1,11 @@
const { kFormatter, fallbackColor } = require("../src/utils");
const { kFormatter, getCardColors, FlexLayout } = require("../src/utils");
const getStyles = require("./getStyles");
const icons = require("./icons");
const createTextNode = ({
icon,
label,
value,
id,
index,
lineHeight,
showIcons,
}) => {
const createTextNode = ({ icon, label, value, id, index, showIcons }) => {
const kValue = kFormatter(value);
const staggerDelay = (index + 3) * 150;
// manually calculating lineHeight based on index instead of using <tspan dy="" />
// to fix firefox layout bug
const lheight = lineHeight * (index + 1);
const translateY = lheight - lineHeight / 2;
const labelOffset = showIcons ? `x="25"` : "";
const iconSvg = showIcons
? `
@@ -26,7 +15,7 @@ const createTextNode = ({
`
: "";
return `
<g class="stagger" style="animation-delay: ${staggerDelay}ms" transform="translate(25, ${translateY})">
<g class="stagger" style="animation-delay: ${staggerDelay}ms" transform="translate(25, 0)">
${iconSvg}
<text class="stat bold" ${labelOffset} y="12.5">${label}:</text>
<text class="stat" x="135" y="12.5" data-testid="${id}">${kValue}</text>
@@ -55,14 +44,19 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
icon_color,
text_color,
bg_color,
theme = "default",
} = options;
const lheight = parseInt(line_height);
const titleColor = fallbackColor(title_color, "#2f80ed");
const iconColor = fallbackColor(icon_color, "#4c71f2");
const textColor = fallbackColor(text_color, "#333");
const bgColor = fallbackColor(bg_color, "#FFFEFE");
// returns theme based colors with proper overrides and defaults
const { titleColor, textColor, iconColor, bgColor } = getCardColors({
title_color,
icon_color,
text_color,
bg_color,
theme,
});
// Meta data for creating text nodes with createTextNode function
const STATS = {
@@ -106,7 +100,6 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
createTextNode({
...STATS[key],
index,
lineHeight: lheight,
showIcons: show_icons,
})
);
@@ -131,15 +124,17 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
});
// Conditionally rendered elements
const apostrophe = ["x", "s"].includes(name.slice(-1)) ? "" : "s";
const title = hide_title
? ""
: `<text x="25" y="35" class="header">${name}'s GitHub Stats</text>`;
: `<text x="25" y="35" class="header">${name}'${apostrophe} GitHub Stats</text>`;
const border = hide_border
? ""
: `
<rect
data-testid="card-border"
data-testid="card-bg"
x="0.5"
y="0.5"
width="494"
@@ -188,8 +183,12 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
})">
${rankCircle}
<svg x="0" y="45">
${statItems.toString().replace(/\,/gm, "")}
<svg x="0" y="55">
${FlexLayout({
items: statItems,
gap: lheight,
direction: "column",
}).join("")}
</svg>
</g>
</svg>
+70
View File
@@ -1,4 +1,5 @@
const axios = require("axios");
const themes = require("../themes");
const renderError = (message) => {
return `
@@ -43,6 +44,10 @@ function parseBoolean(value) {
}
}
function clampValue(number, min, max) {
return Math.max(min, Math.min(number, max));
}
function fallbackColor(color, fallbackColor) {
return (isValidHexColor(color) && `#${color}`) || fallbackColor;
}
@@ -56,6 +61,67 @@ function request(data, headers) {
});
}
/**
*
* @param {String[]} items
* @param {Number} gap
* @param {string} direction
*
* @description
* Auto layout utility, allows us to layout things
* vertically or horizontally with proper gaping
*/
function FlexLayout({ items, gap, direction }) {
// filter() for filtering out empty strings
return items.filter(Boolean).map((item, i) => {
let transform = `translate(${gap * i}, 0)`;
if (direction === "column") {
transform = `translate(0, ${gap * i})`;
}
return `<g transform="${transform}">${item}</g>`;
});
}
// returns theme based colors with proper overrides and defaults
function getCardColors({
title_color,
text_color,
icon_color,
bg_color,
theme,
fallbackTheme = "default",
}) {
const defaultTheme = themes[fallbackTheme];
const selectedTheme = themes[theme] || defaultTheme;
// get the color provided by the user else the theme color
// finally if both colors are invalid fallback to default theme
const titleColor = fallbackColor(
title_color || selectedTheme.title_color,
"#" + defaultTheme.title_color
);
const iconColor = fallbackColor(
icon_color || selectedTheme.icon_color,
"#" + defaultTheme.icon_color
);
const textColor = fallbackColor(
text_color || selectedTheme.text_color,
"#" + defaultTheme.text_color
);
const bgColor = fallbackColor(
bg_color || selectedTheme.bg_color,
"#" + defaultTheme.bg_color
);
return { titleColor, iconColor, textColor, bgColor };
}
const CONSTANTS = {
THIRTY_MINUTES: 1800,
TWO_HOURS: 7200,
ONE_DAY: 86400,
};
module.exports = {
renderError,
kFormatter,
@@ -64,4 +130,8 @@ module.exports = {
request,
parseBoolean,
fallbackColor,
FlexLayout,
getCardColors,
clampValue,
CONSTANTS,
};
+78 -29
View File
@@ -3,7 +3,7 @@ const axios = require("axios");
const MockAdapter = require("axios-mock-adapter");
const api = require("../api/index");
const renderStatsCard = require("../src/renderStatsCard");
const { renderError } = require("../src/utils");
const { renderError, CONSTANTS } = require("../src/utils");
const calculateRank = require("../src/calculateRank");
const stats = {
@@ -55,22 +55,29 @@ const error = {
const mock = new MockAdapter(axios);
const faker = (query, data) => {
const req = {
query: {
username: "anuraghazra",
...query,
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data);
return { req, res };
};
afterEach(() => {
mock.reset();
});
describe("Test /api/", () => {
it("should test the request", async () => {
const req = {
query: {
username: "anuraghazra",
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data);
const { req, res } = faker({}, data);
await api(req, res);
@@ -79,16 +86,7 @@ describe("Test /api/", () => {
});
it("should render error card on error", async () => {
const req = {
query: {
username: "anuraghazra",
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, error);
const { req, res } = faker({}, error);
await api(req, res);
@@ -97,8 +95,8 @@ describe("Test /api/", () => {
});
it("should get the query options", async () => {
const req = {
query: {
const { req, res } = faker(
{
username: "anuraghazra",
hide: `["issues","prs","contribs"]`,
show_icons: true,
@@ -109,12 +107,8 @@ describe("Test /api/", () => {
text_color: "fff",
bg_color: "fff",
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data);
data
);
await api(req, res);
@@ -132,4 +126,59 @@ describe("Test /api/", () => {
})
);
});
it("should have proper cache", async () => {
const { req, res } = faker({}, data);
mock.onPost("https://api.github.com/graphql").reply(200, data);
await api(req, res);
expect(res.setHeader.mock.calls).toEqual([
["Content-Type", "image/svg+xml"],
["Cache-Control", `public, max-age=${CONSTANTS.THIRTY_MINUTES}`],
]);
});
it("should set proper cache", async () => {
const { req, res } = faker({ cache_seconds: 2000 }, data);
await api(req, res);
expect(res.setHeader.mock.calls).toEqual([
["Content-Type", "image/svg+xml"],
["Cache-Control", `public, max-age=${2000}`],
]);
});
it("should set proper cache with clamped values", async () => {
{
let { req, res } = faker({ cache_seconds: 200000 }, data);
await api(req, res);
expect(res.setHeader.mock.calls).toEqual([
["Content-Type", "image/svg+xml"],
["Cache-Control", `public, max-age=${CONSTANTS.ONE_DAY}`],
]);
}
// note i'm using block scoped vars
{
let { req, res } = faker({ cache_seconds: 0 }, data);
await api(req, res);
expect(res.setHeader.mock.calls).toEqual([
["Content-Type", "image/svg+xml"],
["Cache-Control", `public, max-age=${CONSTANTS.THIRTY_MINUTES}`],
]);
}
{
let { req, res } = faker({ cache_seconds: -10000 }, data);
await api(req, res);
expect(res.setHeader.mock.calls).toEqual([
["Content-Type", "image/svg+xml"],
["Cache-Control", `public, max-age=${CONSTANTS.THIRTY_MINUTES}`],
]);
}
});
});
+80 -6
View File
@@ -3,6 +3,7 @@ const cssToObject = require("css-to-object");
const renderRepoCard = require("../src/renderRepoCard");
const { queryByTestId } = require("@testing-library/dom");
const themes = require("../themes");
const data_repo = {
repository: {
@@ -108,13 +109,13 @@ describe("Test renderRepoCard", () => {
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[".header"];
const statClassStyles = stylesObject[".description"];
const descClassStyles = stylesObject[".description"];
const iconClassStyles = stylesObject[".icon"];
expect(headerClassStyles.fill).toBe("#2f80ed");
expect(statClassStyles.fill).toBe("#333");
expect(descClassStyles.fill).toBe("#333");
expect(iconClassStyles.fill).toBe("#586069");
expect(queryByTestId(document.body, "card-border")).toHaveAttribute(
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
"#FFFEFE"
);
@@ -136,18 +137,63 @@ describe("Test renderRepoCard", () => {
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[".header"];
const statClassStyles = stylesObject[".description"];
const descClassStyles = stylesObject[".description"];
const iconClassStyles = stylesObject[".icon"];
expect(headerClassStyles.fill).toBe(`#${customColors.title_color}`);
expect(statClassStyles.fill).toBe(`#${customColors.text_color}`);
expect(descClassStyles.fill).toBe(`#${customColors.text_color}`);
expect(iconClassStyles.fill).toBe(`#${customColors.icon_color}`);
expect(queryByTestId(document.body, "card-border")).toHaveAttribute(
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
"#252525"
);
});
it("should render custom colors with themes", () => {
document.body.innerHTML = renderRepoCard(data_repo.repository, {
title_color: "5a0",
theme: "radical",
});
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[".header"];
const descClassStyles = stylesObject[".description"];
const iconClassStyles = stylesObject[".icon"];
expect(headerClassStyles.fill).toBe("#5a0");
expect(descClassStyles.fill).toBe(`#${themes.radical.text_color}`);
expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`);
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
`#${themes.radical.bg_color}`
);
});
it("should render custom colors with themes and fallback to default colors if invalid", () => {
document.body.innerHTML = renderRepoCard(data_repo.repository, {
title_color: "invalid color",
text_color: "invalid color",
theme: "radical",
});
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[".header"];
const descClassStyles = stylesObject[".description"];
const iconClassStyles = stylesObject[".icon"];
expect(headerClassStyles.fill).toBe(`#${themes.default.title_color}`);
expect(descClassStyles.fill).toBe(`#${themes.default.text_color}`);
expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`);
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
`#${themes.radical.bg_color}`
);
});
it("should render archive badge if repo is archived", () => {
document.body.innerHTML = renderRepoCard({
...data_repo.repository,
@@ -158,4 +204,32 @@ describe("Test renderRepoCard", () => {
"Archived"
);
});
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 },
});
expect(queryByTestId(document.body, "stargazers")).toBeNull();
expect(queryByTestId(document.body, "forkcount")).toBeDefined();
document.body.innerHTML = renderRepoCard({
...data_repo.repository,
stargazers: { totalCount: 1 },
forkCount: 0,
});
expect(queryByTestId(document.body, "stargazers")).toBeDefined();
expect(queryByTestId(document.body, "forkcount")).toBeNull();
document.body.innerHTML = renderRepoCard({
...data_repo.repository,
stargazers: { totalCount: 0 },
forkCount: 0,
});
expect(queryByTestId(document.body, "stargazers")).toBeNull();
expect(queryByTestId(document.body, "forkcount")).toBeNull();
});
});
+64 -6
View File
@@ -7,6 +7,7 @@ const {
queryByTestId,
queryAllByTestId,
} = require("@testing-library/dom");
const themes = require("../themes");
describe("Test renderStatsCard", () => {
const stats = {
@@ -34,10 +35,24 @@ describe("Test renderStatsCard", () => {
expect(getByTestId(document.body, "issues").textContent).toBe("300");
expect(getByTestId(document.body, "prs").textContent).toBe("400");
expect(getByTestId(document.body, "contribs").textContent).toBe("500");
expect(queryByTestId(document.body, "card-border")).toBeInTheDocument();
expect(queryByTestId(document.body, "card-bg")).toBeInTheDocument();
expect(queryByTestId(document.body, "rank-circle")).toBeInTheDocument();
});
it("should have proper name apostrophe", () => {
document.body.innerHTML = renderStatsCard({ ...stats, name: "Anil Das" });
expect(document.getElementsByClassName("header")[0].textContent).toBe(
"Anil Das' GitHub Stats"
);
document.body.innerHTML = renderStatsCard({ ...stats, name: "Felix" });
expect(document.getElementsByClassName("header")[0].textContent).toBe(
"Felix' GitHub Stats"
);
});
it("should hide individual stats", () => {
document.body.innerHTML = renderStatsCard(stats, {
hide: "['issues', 'prs', 'contribs']",
@@ -57,7 +72,7 @@ describe("Test renderStatsCard", () => {
it("should hide_border", () => {
document.body.innerHTML = renderStatsCard(stats, { hide_border: true });
expect(queryByTestId(document.body, "card-border")).not.toBeInTheDocument();
expect(queryByTestId(document.body, "card-bg")).not.toBeInTheDocument();
});
it("should hide_rank", () => {
@@ -70,7 +85,6 @@ describe("Test renderStatsCard", () => {
document.body.innerHTML = renderStatsCard(stats);
const styleTag = document.querySelector("style");
console.log(styleTag.textContent);
const stylesObject = cssToObject(styleTag.textContent);
const headerClassStyles = stylesObject[".header"];
@@ -80,7 +94,7 @@ describe("Test renderStatsCard", () => {
expect(headerClassStyles.fill).toBe("#2f80ed");
expect(statClassStyles.fill).toBe("#333");
expect(iconClassStyles.fill).toBe("#4c71f2");
expect(queryByTestId(document.body, "card-border")).toHaveAttribute(
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
"#FFFEFE"
);
@@ -106,12 +120,57 @@ describe("Test renderStatsCard", () => {
expect(headerClassStyles.fill).toBe(`#${customColors.title_color}`);
expect(statClassStyles.fill).toBe(`#${customColors.text_color}`);
expect(iconClassStyles.fill).toBe(`#${customColors.icon_color}`);
expect(queryByTestId(document.body, "card-border")).toHaveAttribute(
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
"#252525"
);
});
it("should render custom colors with themes", () => {
document.body.innerHTML = renderStatsCard(stats, {
title_color: "5a0",
theme: "radical",
});
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[".header"];
const statClassStyles = stylesObject[".stat"];
const iconClassStyles = stylesObject[".icon"];
expect(headerClassStyles.fill).toBe("#5a0");
expect(statClassStyles.fill).toBe(`#${themes.radical.text_color}`);
expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`);
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
`#${themes.radical.bg_color}`
);
});
it("should render custom colors with themes and fallback to default colors if invalid", () => {
document.body.innerHTML = renderStatsCard(stats, {
title_color: "invalid color",
text_color: "invalid color",
theme: "radical",
});
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[".header"];
const statClassStyles = stylesObject[".stat"];
const iconClassStyles = stylesObject[".icon"];
expect(headerClassStyles.fill).toBe(`#${themes.default.title_color}`);
expect(statClassStyles.fill).toBe(`#${themes.default.text_color}`);
expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`);
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
`#${themes.radical.bg_color}`
);
});
it("should hide the title", () => {
document.body.innerHTML = renderStatsCard(stats, {
hide_title: true,
@@ -157,7 +216,6 @@ describe("Test renderStatsCard", () => {
it("should not have icons if show_icons is false", () => {
document.body.innerHTML = renderStatsCard(stats, { show_icons: false });
console.log(queryAllByTestId(document.body, "icon"));
expect(queryAllByTestId(document.body, "icon")[0]).not.toBeDefined();
expect(queryByTestId(document.body, "stars")).toBeDefined();
expect(
+72 -1
View File
@@ -1,4 +1,10 @@
const { kFormatter, encodeHTML, renderError } = require("../src/utils");
const {
kFormatter,
encodeHTML,
renderError,
FlexLayout,
getCardColors,
} = require("../src/utils");
describe("Test utils.js", () => {
it("should test kFormatter", () => {
@@ -23,4 +29,69 @@ describe("Test utils.js", () => {
"Something went wrong"
);
});
it("should test FlexLayout", () => {
const layout = FlexLayout({
items: ["<text>1</text>", "<text>2</text>"],
gap: 60,
}).join("");
expect(layout).toBe(
`<g transform=\"translate(0, 0)\"><text>1</text></g><g transform=\"translate(60, 0)\"><text>2</text></g>`
);
const columns = FlexLayout({
items: ["<text>1</text>", "<text>2</text>"],
gap: 60,
direction: "column",
}).join("");
expect(columns).toBe(
`<g transform=\"translate(0, 0)\"><text>1</text></g><g transform=\"translate(0, 60)\"><text>2</text></g>`
);
});
it("getCardColors: should return expected values", () => {
let colors = getCardColors({
title_color: "f00",
text_color: "0f0",
icon_color: "00f",
bg_color: "fff",
theme: "dark",
});
expect(colors).toStrictEqual({
titleColor: "#f00",
textColor: "#0f0",
iconColor: "#00f",
bgColor: "#fff",
});
});
it("getCardColors: should fallback to default colors if color is invalid", () => {
let colors = getCardColors({
title_color: "invalidcolor",
text_color: "0f0",
icon_color: "00f",
bg_color: "fff",
theme: "dark",
});
expect(colors).toStrictEqual({
titleColor: "#2f80ed",
textColor: "#0f0",
iconColor: "#00f",
bgColor: "#fff",
});
});
it("getCardColors: should fallback to specified theme colors if is not defined", () => {
let colors = getCardColors({
theme: "dark",
});
expect(colors).toStrictEqual({
titleColor: "#fff",
textColor: "#9f9f9f",
iconColor: "#79ff97",
bgColor: "#151515",
});
});
});
+76
View File
@@ -0,0 +1,76 @@
const themes = {
default: {
title_color: "2f80ed",
icon_color: "4c71f2",
text_color: "333",
bg_color: "FFFEFE",
},
default_repocard: {
title_color: "2f80ed",
icon_color: "586069", // icon color is different
text_color: "333",
bg_color: "FFFEFE",
},
dark: {
title_color: "fff",
icon_color: "79ff97",
text_color: "9f9f9f",
bg_color: "151515",
},
radical: {
title_color: "fe428e",
icon_color: "f8d847",
text_color: "a9fef7",
bg_color: "141321",
},
merko: {
title_color: "abd200",
icon_color: "b7d364",
text_color: "68b587",
bg_color: "0a0f0b",
},
gruvbox: {
title_color: "fabd2f",
icon_color: "fe8019",
text_color: "8ec07c",
bg_color: "282828",
},
tokyonight: {
title_color: "70a5fd",
icon_color: "bf91f3",
text_color: "38bdae",
bg_color: "1a1b27",
},
onedark: {
title_color: "e4bf7a",
icon_color: "8eb573",
text_color: "df6d74",
bg_color: "282c34",
},
cobalt: {
title_color: "e683d9",
icon_color: "0480ef",
text_color: "75eeb2",
bg_color: "193549",
},
synthwave: {
title_color: "e2e9ec",
icon_color: "ef8539",
text_color: "e5289e",
bg_color: "2b213a",
},
highcontrast: {
title_color: "e7f216",
icon_color: "00ffff",
text_color: "fff",
bg_color: "000",
},
dracula: {
title_color: "ff6e96",
icon_color: "79dafa",
text_color: "f8f8f2",
bg_color: "282a36",
},
};
module.exports = themes;