Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdf445d734 | ||
|
|
dd2c7ed278 | ||
|
|
9a7ed8f192 | ||
|
|
71c63abe3a | ||
|
|
30da0119bf | ||
|
|
2c26329e13 | ||
|
|
b4a9bd4468 | ||
|
|
f5cd85f11f | ||
|
|
c436ed1121 | ||
|
|
f6c901b938 | ||
|
|
e9dc35318b | ||
|
|
253eb39b19 | ||
|
|
06f0021660 | ||
|
|
f27daeb931 | ||
|
|
b6aa462881 | ||
|
|
be79fe4051 | ||
|
|
b039fa16af | ||
|
|
61442cb359 | ||
|
|
431f2a0b5b | ||
|
|
2709d5b434 | ||
|
|
96f89ad2b7 | ||
|
|
7dcb60f576 | ||
|
|
d5f410d4ec | ||
|
|
1c0332352a | ||
|
|
4febc6644a | ||
|
|
6fedd69f86 | ||
|
|
1078c85455 | ||
|
|
9e4d83247e | ||
|
|
597dac2985 | ||
|
|
0ea2668176 | ||
|
|
1091fc7695 | ||
|
|
6d4fbfecfb | ||
|
|
331295139d | ||
|
|
a726e8dbaf | ||
|
|
817f736b4e | ||
|
|
987fb0c30d | ||
|
|
96d37c706f | ||
|
|
3a24ad62b1 | ||
|
|
989079e4fd | ||
|
|
95f1233ab9 | ||
|
|
1a53fb9a84 | ||
|
|
579ed2bab8 | ||
|
|
2658a8737d |
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: ["https://www.paypal.me/anuraghazra"] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots / Live demo link (paste the github-readme-stats link as markdown image)**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
+19
-2
@@ -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");
|
||||
|
||||
@@ -7,6 +12,7 @@ module.exports = async (req, res) => {
|
||||
const {
|
||||
username,
|
||||
hide,
|
||||
hide_title,
|
||||
hide_border,
|
||||
hide_rank,
|
||||
show_icons,
|
||||
@@ -15,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 {
|
||||
@@ -27,10 +34,19 @@ 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 || "[]"),
|
||||
show_icons: parseBoolean(show_icons),
|
||||
hide_title: parseBoolean(hide_title),
|
||||
hide_border: parseBoolean(hide_border),
|
||||
hide_rank: parseBoolean(hide_rank),
|
||||
line_height,
|
||||
@@ -38,6 +54,7 @@ module.exports = async (req, res) => {
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
+33
-3
@@ -1,5 +1,10 @@
|
||||
require("dotenv").config();
|
||||
const { renderError } = require("../src/utils");
|
||||
const {
|
||||
renderError,
|
||||
parseBoolean,
|
||||
clampValue,
|
||||
CONSTANTS,
|
||||
} = require("../src/utils");
|
||||
const fetchRepo = require("../src/fetchRepo");
|
||||
const renderRepoCard = require("../src/renderRepoCard");
|
||||
|
||||
@@ -11,11 +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 {
|
||||
@@ -25,12 +32,35 @@ 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),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<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">Get dynamically generated GitHub stats on your readmes!</p>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/actions">
|
||||
@@ -21,12 +23,17 @@
|
||||
·
|
||||
<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!
|
||||
|
||||
# Features
|
||||
|
||||
- [GitHub Stats Card](#github-stats-card)
|
||||
- [GitHub Extra Pins](#github-extra-pins)
|
||||
- [Themes](#themes)
|
||||
- [Customization](#customization)
|
||||
- [Deploy Yourself](#deploy-on-your-own-vercel-instance)
|
||||
|
||||
@@ -46,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
|
||||

|
||||
@@ -60,11 +67,23 @@ To enable icons, you can pass `show_icons=true` in the query param, like so:
|
||||

|
||||
```
|
||||
|
||||
Other options:
|
||||
### Themes
|
||||
|
||||
- `&hide_border=true` hide the border box if you don't like it :D
|
||||
- `&line_height=30` control the line-height between text
|
||||
- `&hide_rank=true` hides the ranking
|
||||
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
|
||||

|
||||
```
|
||||
|
||||
#### 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
|
||||
|
||||
@@ -72,18 +91,21 @@ You can customize the appearance of your `Stats Card` or `Repo Card` however you
|
||||
|
||||
Customization Options:
|
||||
|
||||
| Option | type | Stats Card (default) | Repo Card (default) |
|
||||
| ----------- | --------- | ---------------------- | ---------------------- |
|
||||
| title_color | hex color | #2f80ed | #2f80ed |
|
||||
| text_color | hex color | #333 | #333 |
|
||||
| icon_color | hex color | #4c71f2 | #586069 |
|
||||
| bg_color | hex color | rgba(255, 255, 255, 0) | rgba(255, 255, 255, 0) |
|
||||
| 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 |
|
||||
| theme | string | sets inbuilt theme | 'default' | 'default_repocard' |
|
||||
|
||||
- You can also customize the cards to be compatible with dark mode
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
---
|
||||
|
||||
### Demo
|
||||
|
||||
@@ -99,6 +121,12 @@ Customization Options:
|
||||
|
||||

|
||||
|
||||
- Themes
|
||||
|
||||
Choose from any of the [default themes](#themes)
|
||||
|
||||

|
||||
|
||||
- Customizing stats card
|
||||
|
||||

|
||||
@@ -107,6 +135,8 @@ Customization Options:
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
# GitHub Extra Pins
|
||||
|
||||
GitHub extra pins allow you to pin more than 6 repositories in your profile using a GitHub readme profile.
|
||||
@@ -127,6 +157,10 @@ Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
Use [show_owner](#customization) variable to include the repo's owner username
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
### Quick Tip (Align The Repo Cards)
|
||||
|
||||
You usually won't be able to layout the images side by side. To do that you can use this approach:
|
||||
@@ -144,6 +178,8 @@ You usually won't be able to layout the images side by side. To do that you can
|
||||
|
||||
Since the GitHub API only allows 5k requests per hour, it is possible that my `https://github-readme-stats.vercel.app/api` could hit the rate limiter. If you host it on your own Vercel server, then you don't have to worry about anything. Click on the deploy button to get started!
|
||||
|
||||
NOTE: Since [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) we should be able to handle more than 5k requests and have no issues with downtime :D
|
||||
|
||||
[](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
<details>
|
||||
@@ -151,21 +187,36 @@ Since the GitHub API only allows 5k requests per hour, it is possible that my `h
|
||||
|
||||
1. Go to [vercel.com](https://vercel.com/)
|
||||
1. Click on `Log in`
|
||||

|
||||

|
||||
1. Sign in with GitHub by pressing `Continue with GitHub`
|
||||

|
||||

|
||||
1. Sign into GitHub and allow access to all repositories, if prompted
|
||||
1. Fork this repo
|
||||
1. Go back to your [Vercel dashboard](https://vercel.com/dashboard)
|
||||
1. Select `Import Project`
|
||||

|
||||

|
||||
1. Select `Import Git Repository`
|
||||

|
||||
1. Select root and keep everything as is, just add your environment variable named GITHUB_TOKEN (as shown), which will contain a personal access token (PAT), which you can easily create [here](https://github.com/settings/tokens/new) (leave everything as is, just name it something, it can be anything you want)
|
||||

|
||||

|
||||
1. Select root and keep everything as is, just add your environment variable named PAT_1 (as shown), which will contain a personal access token (PAT), which you can easily create [here](https://github.com/settings/tokens/new) (leave everything as is, just name it something, it can be anything you want)
|
||||

|
||||
1. Click deploy, and you're good to go. See your domains to use the API!
|
||||
</details>
|
||||
|
||||
## :sparkling_heart: Support the project
|
||||
|
||||
I open-source almost everything I can, and I try to reply to everyone needing help using these projects. Obviously,
|
||||
this takes time. You can use this service for free.
|
||||
|
||||
However, if you are using this project and happy with it or just want to encourage me to continue creating stuff, there are few ways you can do it :-
|
||||
|
||||
- Giving proper credit when you use github-readme-stats on your readme, linking back to it :D
|
||||
- Starring and sharing the project :rocket:
|
||||
- [](https://www.paypal.me/anuraghazra) - You can make one-time donations via PayPal. I'll probably buy a ~~coffee~~ tea. :tea:
|
||||
|
||||
Thanks! :heart:
|
||||
|
||||
---
|
||||
|
||||
Contributions are welcomed! <3
|
||||
|
||||
Made with :heart: and JavaScript.
|
||||
|
||||
+194
@@ -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
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
_Note: 排名是基于用户的统计信息计算得出的,详见 [src/calculateRank.js](./src/calculateRank.js)_
|
||||
|
||||
### 隐藏个人统计信息
|
||||
|
||||
想要隐藏某个统计信息, 你可以传递参数 `?hide=`,并将你想要隐藏的项目用数组进行传递。
|
||||
|
||||
> 选项: `&hide=["stars","prs","issues","contribs"]`
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### 显示图标
|
||||
|
||||
想要显示图标, 你可以传递 `show_icons=true`,像这样:
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### 自定义
|
||||
|
||||
你可以为你的 `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
|
||||

|
||||
```
|
||||
|
||||
### Demo
|
||||
|
||||
- 默认
|
||||
|
||||

|
||||
|
||||
- 隐藏某个项目
|
||||
|
||||

|
||||
|
||||
- 显示图标
|
||||
|
||||

|
||||
|
||||
- 自定义添加卡片
|
||||
|
||||

|
||||
|
||||
- 自定义仓库卡片
|
||||
|
||||

|
||||
|
||||
# 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
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Demo
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
Use [show_owner](#自定义) variable to include the repo's owner username
|
||||
|
||||
[](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
|
||||
|
||||
[](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`
|
||||

|
||||
1. 点击 `Continue with GitHub` 通过 GitHub 进行登录
|
||||

|
||||
1. 登录 GitHub 并允许访问所有存储库(如果系统提示)
|
||||
1. Fork 这个仓库
|
||||
1. 返回到你的 [Vercel dashboard](https://vercel.com/dashboard)
|
||||
1. 选择 `Import Project`
|
||||

|
||||
1. 选择 `Import Git Repository`
|
||||

|
||||
1. 选择 root 并将所有内容保持不变,并且只需添加名为 PAT_1 的环境变量(如图所示),其中将包含一个个人访问令牌(PAT),你可以在[这里](https://github.com/settings/tokens/new)轻松创建(保留默认,并且只需要命名下,名字随便)
|
||||

|
||||
1. 点击 deploy, 这就完成了,查看你的域名就可使用 API 了!
|
||||
</details>
|
||||
|
||||
## :sparkling_heart: 支持这个项目
|
||||
|
||||
我几乎将我所能的进行开源,并且我试着回复每个在使用这些项目时需要帮助的人。很明显,这需要时间,但你可以免费享受此服务。
|
||||
|
||||
然而, 如果你正在使用这个项目并且感觉挺开心或者只是想要支持我继续开发,这里有一些方法:
|
||||
|
||||
- 在你的 README 中使用 github-readme-stats 时,链接指向会这里 :D
|
||||
- Star 并 分享这个项目 :rocket:
|
||||
- [](https://www.paypal.me/anuraghazra) - 你可以通过 PayPal 一次性捐款. 我可能会买一杯~~咖啡~~茶. :tea:
|
||||
|
||||
谢谢! :heart:
|
||||
|
||||
---
|
||||
|
||||
欢迎贡献! <3
|
||||
|
||||
用 :heart: 发电,用 JavaScript 制作。
|
||||
+60
-19
@@ -1,3 +1,21 @@
|
||||
// https://stackoverflow.com/a/5263759/10629172
|
||||
function normalcdf(mean, sigma, to) {
|
||||
var z = (to - mean) / Math.sqrt(2 * sigma * sigma);
|
||||
var t = 1 / (1 + 0.3275911 * Math.abs(z));
|
||||
var a1 = 0.254829592;
|
||||
var a2 = -0.284496736;
|
||||
var a3 = 1.421413741;
|
||||
var a4 = -1.453152027;
|
||||
var a5 = 1.061405429;
|
||||
var erf =
|
||||
1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-z * z);
|
||||
var sign = 1;
|
||||
if (z < 0) {
|
||||
sign = -1;
|
||||
}
|
||||
return (1 / 2) * (1 + sign * erf);
|
||||
}
|
||||
|
||||
function calculateRank({
|
||||
totalRepos,
|
||||
totalCommits,
|
||||
@@ -13,12 +31,24 @@ function calculateRank({
|
||||
const STARS_OFFSET = 0.75;
|
||||
const PRS_OFFSET = 0.5;
|
||||
const FOLLOWERS_OFFSET = 0.45;
|
||||
const REPO_OFFSET = 1;
|
||||
|
||||
const FIRST_STEP = 0;
|
||||
const SECOND_STEP = 5;
|
||||
const THIRD_STEP = 20;
|
||||
const FOURTH_STEP = 50;
|
||||
const FIFTH_STEP = 130;
|
||||
const ALL_OFFSETS =
|
||||
CONTRIBS_OFFSET +
|
||||
ISSUES_OFFSET +
|
||||
STARS_OFFSET +
|
||||
PRS_OFFSET +
|
||||
FOLLOWERS_OFFSET +
|
||||
REPO_OFFSET;
|
||||
|
||||
const RANK_S_VALUE = 1;
|
||||
const RANK_DOUBLE_A_VALUE = 25;
|
||||
const RANK_A2_VALUE = 45;
|
||||
const RANK_A3_VALUE = 60;
|
||||
const RANK_B_VALUE = 100;
|
||||
|
||||
const TOTAL_VALUES =
|
||||
RANK_S_VALUE + RANK_A2_VALUE + RANK_A3_VALUE + RANK_B_VALUE;
|
||||
|
||||
// prettier-ignore
|
||||
const score = (
|
||||
@@ -27,26 +57,37 @@ function calculateRank({
|
||||
issues * ISSUES_OFFSET +
|
||||
stargazers * STARS_OFFSET +
|
||||
prs * PRS_OFFSET +
|
||||
followers * FOLLOWERS_OFFSET
|
||||
) / totalRepos;
|
||||
followers * FOLLOWERS_OFFSET +
|
||||
totalRepos * REPO_OFFSET
|
||||
) / 100;
|
||||
|
||||
const normalizedScore = normalcdf(score, TOTAL_VALUES, ALL_OFFSETS) * 100;
|
||||
|
||||
let level = "";
|
||||
|
||||
if (score == FIRST_STEP) {
|
||||
level = "B";
|
||||
} else if (score > FIRST_STEP && score <= SECOND_STEP) {
|
||||
level = "B+";
|
||||
} else if (score > SECOND_STEP && score <= THIRD_STEP) {
|
||||
level = "A";
|
||||
} else if (score > THIRD_STEP && score <= FOURTH_STEP) {
|
||||
level = "A+";
|
||||
} else if (score > FOURTH_STEP && score <= FIFTH_STEP) {
|
||||
level = "A++";
|
||||
} else if (score > FIFTH_STEP) {
|
||||
if (normalizedScore < RANK_S_VALUE) {
|
||||
level = "S+";
|
||||
}
|
||||
if (
|
||||
normalizedScore >= RANK_S_VALUE &&
|
||||
normalizedScore < RANK_DOUBLE_A_VALUE
|
||||
) {
|
||||
level = "S";
|
||||
}
|
||||
if (
|
||||
normalizedScore >= RANK_DOUBLE_A_VALUE &&
|
||||
normalizedScore < RANK_A2_VALUE
|
||||
) {
|
||||
level = "A++";
|
||||
}
|
||||
if (normalizedScore >= RANK_A2_VALUE && normalizedScore < RANK_A3_VALUE) {
|
||||
level = "A+";
|
||||
}
|
||||
if (normalizedScore >= RANK_A3_VALUE && normalizedScore < RANK_B_VALUE) {
|
||||
level = "B+";
|
||||
}
|
||||
|
||||
return { level, score };
|
||||
return { level, score: normalizedScore };
|
||||
}
|
||||
|
||||
module.exports = calculateRank;
|
||||
|
||||
+13
-4
@@ -7,6 +7,9 @@ const fetcher = (variables, token) => {
|
||||
query: `
|
||||
fragment RepoInfo on Repository {
|
||||
name
|
||||
nameWithOwner
|
||||
isPrivate
|
||||
isArchived
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
@@ -52,15 +55,21 @@ async function fetchRepo(username, reponame) {
|
||||
throw new Error("Not found");
|
||||
}
|
||||
|
||||
if (data.organization === null && data.user) {
|
||||
if (!data.user.repository) {
|
||||
const isUser = data.organization === null && data.user;
|
||||
const isOrg = data.user === null && data.organization;
|
||||
|
||||
if (isUser) {
|
||||
if (!data.user.repository || data.user.repository.isPrivate) {
|
||||
throw new Error("User Repository Not found");
|
||||
}
|
||||
return data.user.repository;
|
||||
}
|
||||
|
||||
if (data.user === null && data.organization) {
|
||||
if (!data.organization.repository) {
|
||||
if (isOrg) {
|
||||
if (
|
||||
!data.organization.repository ||
|
||||
data.organization.repository.isPrivate
|
||||
) {
|
||||
throw new Error("Organization Repository Not found");
|
||||
}
|
||||
return data.organization.repository;
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ const fetcher = (variables, token) => {
|
||||
followers {
|
||||
totalCount
|
||||
}
|
||||
repositories(first: 100, orderBy: { direction: DESC, field: STARGAZERS }) {
|
||||
repositories(first: 100, ownerAffiliations: OWNER, isFork: false, orderBy: {direction: DESC, field: STARGAZERS}) {
|
||||
totalCount
|
||||
nodes {
|
||||
stargazers {
|
||||
|
||||
+1
-4
@@ -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;
|
||||
@@ -64,9 +64,6 @@ const getStyles = ({
|
||||
}
|
||||
|
||||
.bold { font-weight: 700 }
|
||||
.star-icon {
|
||||
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
|
||||
}
|
||||
.icon {
|
||||
fill: ${iconColor};
|
||||
display: ${!!show_icons ? "block" : "none"};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
const icons = {
|
||||
star: `<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>`,
|
||||
commits: `<path fill-rule="evenodd" d="M1.643 3.143L.427 1.927A.25.25 0 000 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 00.177-.427L2.715 4.215a6.5 6.5 0 11-1.18 4.458.75.75 0 10-1.493.154 8.001 8.001 0 101.6-5.684zM7.75 4a.75.75 0 01.75.75v2.992l2.028.812a.75.75 0 01-.557 1.392l-2.5-1A.75.75 0 017 8.25v-3.5A.75.75 0 017.75 4z"/>`,
|
||||
prs: `<path fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"/>`,
|
||||
issues: `<path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"/>`,
|
||||
icon: `<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>`,
|
||||
contribs: `<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>`,
|
||||
fork: `<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path>`,
|
||||
};
|
||||
|
||||
module.exports = icons;
|
||||
+79
-26
@@ -1,9 +1,31 @@
|
||||
const { kFormatter, encodeHTML, fallbackColor } = require("../src/utils");
|
||||
const {
|
||||
kFormatter,
|
||||
encodeHTML,
|
||||
getCardColors,
|
||||
FlexLayout,
|
||||
} = require("../src/utils");
|
||||
const icons = require("./icons");
|
||||
|
||||
const renderRepoCard = (repo, options = {}) => {
|
||||
const { name, description, primaryLanguage, stargazers, forkCount } = repo;
|
||||
const { title_color, icon_color, text_color, bg_color } = options;
|
||||
const {
|
||||
name,
|
||||
nameWithOwner,
|
||||
description,
|
||||
primaryLanguage,
|
||||
stargazers,
|
||||
isArchived,
|
||||
forkCount,
|
||||
} = repo;
|
||||
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";
|
||||
const langColor = primaryLanguage ? primaryLanguage.color : "#333";
|
||||
|
||||
@@ -15,47 +37,78 @@ 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);
|
||||
|
||||
const archiveBadge = isArchived
|
||||
? `
|
||||
<g data-testid="archive-badge" class="archive-badge" transform="translate(320, 38)">
|
||||
<rect stroke="${textColor}" stroke-width="1" width="70" height="20" x="-12" y="-14" ry="10" rx="10"></rect>
|
||||
<text fill="${textColor}">Archived</text>
|
||||
</g>
|
||||
`
|
||||
: "";
|
||||
|
||||
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 width="400" height="${height}" viewBox="0 0 400 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg version="1.1" width="400" height="${height}" viewBox="0 0 400 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.header { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor} }
|
||||
.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} }
|
||||
.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">
|
||||
<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"></path>
|
||||
${icons.contribs}
|
||||
</svg>
|
||||
|
||||
<text x="50" y="38" class="header">${name}</text>
|
||||
${archiveBadge}
|
||||
|
||||
<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">
|
||||
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path>
|
||||
</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">
|
||||
<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path>
|
||||
</svg>
|
||||
<text data-testid="forkcount" class="gray" x="25">${totalForks}</text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
|
||||
+90
-58
@@ -1,21 +1,25 @@
|
||||
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 }) => {
|
||||
const classname = icon === "★" && "star-icon";
|
||||
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 labelOffset = showIcons ? `x="25"` : "";
|
||||
const iconSvg = showIcons
|
||||
? `
|
||||
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
${icon}
|
||||
</svg>
|
||||
`
|
||||
: "";
|
||||
return `
|
||||
<text class="stagger" style="animation-delay: ${staggerDelay}ms" x="25" y="${lheight}">
|
||||
<tspan dx="0" data-testid="icon" class="icon ${classname}">${icon}</tspan>
|
||||
<tspan dx="0" class="stat bold">
|
||||
${label}:
|
||||
</tspan>
|
||||
<tspan x="160" data-testid="${id}" class="stat">${kValue}</tspan>
|
||||
</text>
|
||||
<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>
|
||||
</g>
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -32,6 +36,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
const {
|
||||
hide = [],
|
||||
show_icons = false,
|
||||
hide_title = false,
|
||||
hide_border = false,
|
||||
hide_rank = false,
|
||||
line_height = 25,
|
||||
@@ -39,65 +44,97 @@ 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 = {
|
||||
stars: {
|
||||
icon: "★",
|
||||
icon: icons.star,
|
||||
label: "Total Stars",
|
||||
value: totalStars,
|
||||
id: "stars",
|
||||
},
|
||||
commits: {
|
||||
icon: "🕗",
|
||||
icon: icons.commits,
|
||||
label: "Total Commits",
|
||||
value: totalCommits,
|
||||
id: "commits",
|
||||
},
|
||||
prs: {
|
||||
icon: "🔀",
|
||||
icon: icons.prs,
|
||||
label: "Total PRs",
|
||||
value: totalPRs,
|
||||
id: "prs",
|
||||
},
|
||||
issues: {
|
||||
icon: "ⓘ",
|
||||
icon: icons.issues,
|
||||
label: "Total Issues",
|
||||
value: totalIssues,
|
||||
id: "issues",
|
||||
},
|
||||
contribs: {
|
||||
icon: "📕",
|
||||
icon: icons.contribs,
|
||||
label: "Contributed to",
|
||||
value: contributedTo,
|
||||
id: "contribs",
|
||||
},
|
||||
};
|
||||
|
||||
// filter out hidden stats defined by user & create the text nodes
|
||||
const statItems = Object.keys(STATS)
|
||||
.filter((key) => !hide.includes(key))
|
||||
.map((key, index) =>
|
||||
// create the text nodes, and pass index so that we can calculate the line spacing
|
||||
createTextNode({ ...STATS[key], index, lineHeight: lheight })
|
||||
createTextNode({
|
||||
...STATS[key],
|
||||
index,
|
||||
showIcons: show_icons,
|
||||
})
|
||||
);
|
||||
|
||||
// Calculate the card height depending on how many items there are
|
||||
// but if rank circle is visible clamp the minimum height to `150`
|
||||
const height = Math.max(
|
||||
let height = Math.max(
|
||||
45 + (statItems.length + 1) * lheight,
|
||||
hide_rank ? 0 : 150
|
||||
);
|
||||
|
||||
const border = `
|
||||
// the better user's score the the rank will be closer to zero so
|
||||
// subtracting 100 to get the progress in 100%
|
||||
const progress = 100 - rank.score;
|
||||
|
||||
const styles = getStyles({
|
||||
titleColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
show_icons,
|
||||
progress,
|
||||
});
|
||||
|
||||
// Conditionally rendered elements
|
||||
|
||||
const apostrophe = ["x", "s"].includes(name.slice(-1)) ? "" : "s";
|
||||
const title = hide_title
|
||||
? ""
|
||||
: `<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"
|
||||
@@ -115,34 +152,22 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
})">
|
||||
<circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
|
||||
<circle class="rank-circle" cx="-10" cy="8" r="40" />
|
||||
<text
|
||||
x="${rank.level.length === 1 ? "-4" : "0"}"
|
||||
y="0"
|
||||
alignment-baseline="central"
|
||||
dominant-baseline="central"
|
||||
text-anchor="middle"
|
||||
class="rank-text"
|
||||
>
|
||||
${rank.level}
|
||||
</text>
|
||||
<g class="rank-text">
|
||||
<text
|
||||
x="${rank.level.length === 1 ? "-4" : "0"}"
|
||||
y="0"
|
||||
alignment-baseline="central"
|
||||
dominant-baseline="central"
|
||||
text-anchor="middle"
|
||||
>
|
||||
${rank.level}
|
||||
</text>
|
||||
</g>
|
||||
</g>`;
|
||||
|
||||
// re-adjust circle progressbar's value until the ranking algo is improved
|
||||
let progress = rank.score;
|
||||
if (rank.score > 86) {
|
||||
progress = (40 + rank.score) * 0.6;
|
||||
if (hide_title) {
|
||||
height -= 30;
|
||||
}
|
||||
if (rank.score < 40) {
|
||||
progress = 40 + rank.score;
|
||||
}
|
||||
|
||||
const styles = getStyles({
|
||||
titleColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
show_icons,
|
||||
progress,
|
||||
});
|
||||
|
||||
return `
|
||||
<svg width="495" height="${height}" viewBox="0 0 495 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -150,15 +175,22 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
${styles}
|
||||
</style>
|
||||
|
||||
${hide_border ? "" : border}
|
||||
${border}
|
||||
${title}
|
||||
|
||||
${rankCircle}
|
||||
|
||||
<text x="25" y="35" class="header">${name}'s GitHub Stats</text>
|
||||
<g data-testid="card-body-content" transform="translate(0, ${
|
||||
hide_title ? -30 : 0
|
||||
})">
|
||||
${rankCircle}
|
||||
|
||||
<svg x="0" y="45">
|
||||
${statItems.toString().replace(/\,/gm, "")}
|
||||
</svg>
|
||||
<svg x="0" y="55">
|
||||
${FlexLayout({
|
||||
items: statItems,
|
||||
gap: lheight,
|
||||
direction: "column",
|
||||
}).join("")}
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
|
||||
+75
-11
@@ -1,4 +1,5 @@
|
||||
const axios = require("axios");
|
||||
const themes = require("../themes");
|
||||
|
||||
const renderError = (message) => {
|
||||
return `
|
||||
@@ -43,25 +44,84 @@ 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;
|
||||
}
|
||||
|
||||
function request(data, headers) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios({
|
||||
url: "https://api.github.com/graphql",
|
||||
method: "post",
|
||||
headers: {
|
||||
...headers,
|
||||
},
|
||||
data,
|
||||
})
|
||||
.then((response) => resolve(response))
|
||||
.catch((error) => reject(error));
|
||||
return axios({
|
||||
url: "https://api.github.com/graphql",
|
||||
method: "post",
|
||||
headers,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @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,
|
||||
@@ -70,4 +130,8 @@ module.exports = {
|
||||
request,
|
||||
parseBoolean,
|
||||
fallbackColor,
|
||||
FlexLayout,
|
||||
getCardColors,
|
||||
clampValue,
|
||||
CONSTANTS,
|
||||
};
|
||||
|
||||
+78
-29
@@ -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}`],
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,6 @@ describe("Test calculateRank", () => {
|
||||
prs: 300,
|
||||
issues: 200,
|
||||
})
|
||||
).toStrictEqual({ level: "S+", score: 192.13 });
|
||||
).toStrictEqual({ level: "A+", score: 49.16605417270399 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,4 +80,17 @@ describe("Test fetchRepo", () => {
|
||||
"Not found"
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error if repository is private", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, {
|
||||
data: {
|
||||
user: { repository: { ...data_repo, isPrivate: true } },
|
||||
organization: null,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow(
|
||||
"User Repository Not found"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ const { renderError } = require("../src/utils");
|
||||
|
||||
const data_repo = {
|
||||
repository: {
|
||||
username: "anuraghazra",
|
||||
name: "convoychat",
|
||||
stargazers: { totalCount: 38000 },
|
||||
description: "Help us take over the world! React + TS + GraphQL Chat App",
|
||||
@@ -61,6 +62,7 @@ describe("Test /api/pin", () => {
|
||||
icon_color: "fff",
|
||||
text_color: "fff",
|
||||
bg_color: "fff",
|
||||
full_name: "1",
|
||||
},
|
||||
};
|
||||
const res = {
|
||||
|
||||
@@ -3,9 +3,11 @@ 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: {
|
||||
nameWithOwner: "anuraghazra/convoychat",
|
||||
name: "convoychat",
|
||||
stargazers: { totalCount: 38000 },
|
||||
description: "Help us take over the world! React + TS + GraphQL Chat App",
|
||||
@@ -22,9 +24,10 @@ describe("Test renderRepoCard", () => {
|
||||
it("should render correctly", () => {
|
||||
document.body.innerHTML = renderRepoCard(data_repo.repository);
|
||||
|
||||
expect(document.getElementsByClassName("header")[0]).toHaveTextContent(
|
||||
"convoychat"
|
||||
);
|
||||
const [header] = document.getElementsByClassName("header");
|
||||
|
||||
expect(header).toHaveTextContent("convoychat");
|
||||
expect(header).not.toHaveTextContent("anuraghazra");
|
||||
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
|
||||
"Help us take over the world! React + TS + GraphQL Chat .."
|
||||
);
|
||||
@@ -39,6 +42,15 @@ describe("Test renderRepoCard", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should display username in title (full repo name)", () => {
|
||||
document.body.innerHTML = renderRepoCard(data_repo.repository, {
|
||||
show_owner: true,
|
||||
});
|
||||
expect(document.getElementsByClassName("header")[0]).toHaveTextContent(
|
||||
"anuraghazra/convoychat"
|
||||
);
|
||||
});
|
||||
|
||||
it("should trim description", () => {
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
@@ -97,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"
|
||||
);
|
||||
@@ -125,15 +137,99 @@ 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,
|
||||
isArchived: true,
|
||||
});
|
||||
|
||||
expect(queryByTestId(document.body, "archive-badge")).toHaveTextContent(
|
||||
"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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,12 @@ require("@testing-library/jest-dom");
|
||||
const cssToObject = require("css-to-object");
|
||||
const renderStatsCard = require("../src/renderStatsCard");
|
||||
|
||||
const { getByTestId, queryByTestId } = require("@testing-library/dom");
|
||||
const {
|
||||
getByTestId,
|
||||
queryByTestId,
|
||||
queryAllByTestId,
|
||||
} = require("@testing-library/dom");
|
||||
const themes = require("../themes");
|
||||
|
||||
describe("Test renderStatsCard", () => {
|
||||
const stats = {
|
||||
@@ -30,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']",
|
||||
@@ -53,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", () => {
|
||||
@@ -66,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"];
|
||||
@@ -76,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"
|
||||
);
|
||||
@@ -102,9 +120,106 @@ 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,
|
||||
});
|
||||
|
||||
expect(document.getElementsByClassName("header")[0]).toBeUndefined();
|
||||
expect(document.getElementsByTagName("svg")[0]).toHaveAttribute(
|
||||
"height",
|
||||
"165"
|
||||
);
|
||||
expect(queryByTestId(document.body, "card-body-content")).toHaveAttribute(
|
||||
"transform",
|
||||
"translate(0, -30)"
|
||||
);
|
||||
});
|
||||
|
||||
it("should not hide the title", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, {});
|
||||
|
||||
expect(document.getElementsByClassName("header")[0]).toBeDefined();
|
||||
expect(document.getElementsByTagName("svg")[0]).toHaveAttribute(
|
||||
"height",
|
||||
"195"
|
||||
);
|
||||
expect(queryByTestId(document.body, "card-body-content")).toHaveAttribute(
|
||||
"transform",
|
||||
"translate(0, 0)"
|
||||
);
|
||||
});
|
||||
|
||||
it("should render icons correctly", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
show_icons: true,
|
||||
});
|
||||
|
||||
expect(queryAllByTestId(document.body, "icon")[0]).toBeDefined();
|
||||
expect(queryByTestId(document.body, "stars")).toBeDefined();
|
||||
expect(
|
||||
queryByTestId(document.body, "stars").previousElementSibling // the label
|
||||
).toHaveAttribute("x", "25");
|
||||
});
|
||||
|
||||
it("should not have icons if show_icons is false", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, { show_icons: false });
|
||||
|
||||
expect(queryAllByTestId(document.body, "icon")[0]).not.toBeDefined();
|
||||
expect(queryByTestId(document.body, "stars")).toBeDefined();
|
||||
expect(
|
||||
queryByTestId(document.body, "stars").previousElementSibling // the label
|
||||
).not.toHaveAttribute("x");
|
||||
});
|
||||
});
|
||||
|
||||
+72
-1
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user