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
anuraghazra b6aa462881 design: make badge look good on different colors 2020-07-18 20:28:01 +05:30
anuraghazra be79fe4051 feat: show archive badge if repo is archive 2020-07-18 20:20:36 +05:30
Anurag Hazra b039fa16af Merge pull request #86 from edisonlee55/master
fix: fixed CSS scale() on WebKit causing rendering issues

So I have used the solution provided here (Reference):
CSS transform on SVG text element not working in Safari - Stack Overflow - https://stackoverflow.com/a/61398694/4333875

They have encountered issue with `transform: translate()` and filed a bug report, but `transform: scale()` is also having issue on our side.

Also, the minimum reproducible example is [here](https://stackoverflow.com/questions/62964270/css-scale-animation-in-svg-text-tag-not-working-on-safari) on Stack Overflow.
(the word `Test` won't show up **on Safari**)
2020-07-18 18:06:57 +05:30
Edison Lee 61442cb359 fix: fixed CSS scale() issue on WebKit by wrap up text in g tag 2020-07-18 10:37:34 +08:00
anuraghazra 431f2a0b5b docs: update options table 2020-07-17 23:03:46 +05:30
Anurag Hazra 2709d5b434 feat: added hide_title option (#88) 2020-07-17 23:01:16 +05:30
Anurag Hazra 96f89ad2b7 Merge pull request #81 from reed-jones/patch-1
docs: improved customization table
2020-07-17 20:54:48 +05:30
Reed Jones 7dcb60f576 update default bg_color 2020-07-17 09:16:29 -06:00
Anurag Hazra d5f410d4ec Merge pull request #84 from anuraghazra/fix-private-repos
fix: filter out private repos
2020-07-17 12:46:33 +05:30
anuraghazra 1c0332352a fix: filter out private repos
filtering out private repos just in case, otherwise if a user somehow
found out the name of any of the PAT owner's private -
(considering they created the PAT with private repo access) repos's name
then they could see the repo in github extra pins.
2020-07-17 12:39:19 +05:30
Reed Jones 4febc6644a Changed hex colors to be copy/paste workable
I was a little confused at first setting the bg_color, since it said `hex color` but had `rgba(...)` so I copied the title color which of course didn't work since it started with `#` I think this makes it a bit clearer how to use it!
2020-07-16 17:08:43 -06:00
Anurag Hazra 6fedd69f86 docs: added support the project section 2020-07-16 23:27:23 +05:30
Anurag Hazra 1078c85455 docs: added github-readme-stats logo! 2020-07-16 19:35:18 +05:30
omrilotan 9e4d83247e feat: display username in repo card (optional) (#78)
* Display username in repo card (optional)

* fix: show_owner boolean flag & used nameWithOwner from gql api
2020-07-16 18:03:33 +05:30
anuraghazra 597dac2985 Merge branch 'master' of https://github.com/anuraghazra/github-readme-stats 2020-07-16 17:22:59 +05:30
anuraghazra 0ea2668176 docs: updated vercel deploy docs 2020-07-16 17:22:02 +05:30
Anurag Hazra 1091fc7695 chore: Update issue templates 2020-07-16 16:18:44 +05:30
6d4fbfecfb perf: align icons and text vertical (#33)
* improve: improved rating algorithm wip

* Fixed typos, punctuation [...]

Corrected many instances of "Github" to "GitHub", fixed the punctuation on some sections, fixed the ~lack of~ uppercase chars in others, tweaked the grammar a bit, and added the Vercel guide to a spoiler-ish section so it's only visible if you click to expand.

<3

* fix: github rate limiter with multiple PATs

* perf: vertically align text left

* refactor: refactored retryer logic & handled invalid tokens

* chore: remove redundant codes

`axios` is Promise based, there is no need to wrap it into a Promise constructor again

* fix: query param booleans

* design: fixed rank alignment

* chore: rebase from master

* fix: fixed repo card breaking in absence of primaryLanguage

* fix: fixed stars count #39 & fixed progressbar percentage

* perf: replace emoji icons with GitHub SVG icons

* chore: added funding link

* refactor: refacted icons to another file

* test: added test for icons

Co-authored-by: anuraghazra <hazru.anurag@gmail.com>
Co-authored-by: Micael Jarniac <micael@jarniac.com>
Co-authored-by: JounQin <admin@1stg.me>
2020-07-16 15:51:21 +05:30
Anurag Hazra 331295139d Merge pull request #77 from anuraghazra/funding
chore: added funding link
2020-07-16 14:06:20 +05:30
anuraghazra a726e8dbaf chore: added funding link
Test / build (push) Canceled after 0s
2020-07-16 14:02:15 +05:30
Anurag Hazra 817f736b4e Merge pull request #61 from JounQin/patch-1
chore: remove redundant codes
2020-07-15 20:37:42 +05:30
Anurag Hazra 987fb0c30d Merge pull request #49 from anuraghazra/ranking-algo
improve: improved rating algorithm wip

fix: fixed #39
2020-07-15 20:10:53 +05:30
anuraghazra 96d37c706f fix: fixed stars count #39 & fixed progressbar percentage
Test / build (push) Canceled after 0s
2020-07-15 20:00:22 +05:30
anuraghazra 3a24ad62b1 Merge branch 'master' into ranking-algo 2020-07-15 19:47:07 +05:30
Anurag Hazra 989079e4fd Merge pull request #64 from anuraghazra/fix-nolang
fix: fixed repo card breaking in absence of primaryLanguage
2020-07-15 19:39:40 +05:30
anuraghazra 20bddc3a0c fix: fixed repo card breaking in absence of primaryLanguage
Test / build (push) Canceled after 0s
2020-07-15 19:37:04 +05:30
JounQin 95f1233ab9 chore: rebase from master 2020-07-15 21:55:04 +08:00
JounQin 1a53fb9a84 Merge branch 'master' into patch-1 2020-07-15 21:41:48 +08:00
Anurag Hazra 19bacf780a Merge pull request #63 from anuraghazra/fix-rank-alignment
design: fixed rank alignment
2020-07-15 19:01:39 +05:30
anuraghazra 89bb8fea42 design: fixed rank alignment 2020-07-15 18:59:38 +05:30
Anurag Hazra 0bade9be77 Merge pull request #62 from anuraghazra/fix-queryparams
fix: query param booleans
2020-07-15 18:34:19 +05:30
anuraghazra 5fce46d67c fix: query param booleans
Test / build (push) Canceled after 0s
2020-07-15 18:32:16 +05:30
Anurag Hazra 00ce9d37d2 Merge pull request #54 from MicaelJarniac/patch-1
docs: Fixed typos, punctuation
2020-07-15 18:16:27 +05:30
Anurag Hazra 9328a57ea6 Merge pull request #58 from anuraghazra/fix-ratelimit
fix: increase github rate limit with multiple PATs

- special thanks to @filiptronicek @ApurvShah007 @garvit-joshi for helping out :D
2020-07-15 17:53:43 +05:30
JounQin 579ed2bab8 chore: remove redundant codes
`axios` is Promise based, there is no need to wrap it into a Promise constructor again
2020-07-15 20:22:06 +08:00
anuraghazra 429d65a52b refactor: refactored retryer logic & handled invalid tokens
Test / build (push) Canceled after 0s
2020-07-15 17:46:31 +05:30
anuraghazra 3d8dea90e2 fix: github rate limiter with multiple PATs 2020-07-15 14:59:25 +05:30
Micael Jarniac 47370adbbb Fixed typos, punctuation [...]
Corrected many instances of "Github" to "GitHub", fixed the punctuation on some sections, fixed the ~lack of~ uppercase chars in others, tweaked the grammar a bit, and added the Vercel guide to a spoiler-ish section so it's only visible if you click to expand.

<3
2020-07-14 18:08:52 -03:00
Anurag Hazra 2efb399f33 Merge pull request #53 from filiptronicek/master
docs: Add "Host yourself" docs
2020-07-15 01:19:12 +05:30
Filip Troníček dcde5f8205 Crop the image a bit 2020-07-14 21:40:38 +02:00
Filip Troníček 3b3ce41ae4 Fix img positions 2020-07-14 21:32:06 +02:00
Filip Troníček e3519ceb4c Add documentation for Vercel setup 2020-07-14 21:30:03 +02:00
Anurag Hazra c297798889 Merge pull request #50 from anuraghazra/animations
feat: added animations!
2020-07-14 21:04:11 +05:30
anuraghazra 6059cb1691 feat: added animations!
Test / build (push) Canceled after 0s
2020-07-14 20:04:36 +05:30
anuraghazra 2658a8737d improve: improved rating algorithm wip 2020-07-14 14:08:57 +05:30
Anurag Hazra cbd0e583ff Merge pull request #48 from anuraghazra/fix-circl-rank-rim
design: added a rim to circle rank's progress
2020-07-14 13:12:56 +05:30
anuraghazra 6f79b495a6 design: added a rim to circle rank's progress 2020-07-14 13:02:45 +05:30
Anurag Hazra 014f710e12 fix: firefox layout breaking (#45)
* fix: firefox layout breaking

* refactor: refactored code and added comments

* chore: remove useragent dep
2020-07-13 23:47:04 +05:30
Anurag Hazra 29d3ea63f8 Merge pull request #40 from anuraghazra/ranking
feat: added rankings
2020-07-13 20:04:52 +05:30
26 changed files with 1470 additions and 250 deletions
+12
View File
@@ -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']
+20
View File
@@ -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.
+20
View File
@@ -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.
+23 -4
View File
@@ -1,5 +1,10 @@
require("dotenv").config();
const { renderError } = 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,27 +21,40 @@ module.exports = async (req, res) => {
icon_color,
text_color,
bg_color,
theme,
cache_seconds,
} = req.query;
let stats;
res.setHeader("Content-Type", "image/svg+xml");
try {
stats = await fetchStats(username);
} catch (err) {
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,
hide_border,
hide_rank,
show_icons: parseBoolean(show_icons),
hide_title: parseBoolean(hide_title),
hide_border: parseBoolean(hide_border),
hide_rank: parseBoolean(hide_rank),
line_height,
title_color,
icon_color,
text_color,
bg_color,
theme,
})
);
};
+33 -1
View File
@@ -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,9 +16,13 @@ module.exports = async (req, res) => {
icon_color,
text_color,
bg_color,
theme,
show_owner,
cache_seconds,
} = req.query;
let repoData;
res.setHeader("Content-Type", "image/svg+xml");
try {
@@ -23,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),
})
);
};
+102 -34
View File
@@ -1,6 +1,8 @@
<p align="center">
<h2 align="center">Github Readme Stats</h2>
<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,32 +23,37 @@
·
<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)
- [GitHub Stats Card](#github-stats-card)
- [GitHub Extra Pins](#github-extra-pins)
- [Themes](#themes)
- [Customization](#customization)
- [Deploy Yourself](#deploy-on-your-own-vercel-instance)
# Github Stats Card
# GitHub Stats Card
Copy paste this into your markdown content, and that's it. Simple!
change the `?username=` value to your GitHubs's username
Change the `?username=` value to your GitHub's username.
```md
[![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats)
```
_Note: Ranks are calculated based on users stats, see [src/calculateRank.js](./src/calculateRank.js)_
_Note: Ranks are calculated based on user's stats, see [src/calculateRank.js](./src/calculateRank.js)_
### Hiding individual stats
To hide any specific stats, you can pass a query parameter `?hide=` with an array of items, you wanna hide.
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"])
@@ -54,36 +61,51 @@ To hide any specific stats, you can pass a query parameter `?hide=` with an arra
### Showing icons
To enable icons, you can pass `show_icons=true` in the query param like so
To enable icons, you can pass `show_icons=true` in the query param, like so:
```md
![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true)
```
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
![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.
You can customize the appearance of your `Stats Card` or `Repo Card` however you want with URL params.
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
![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
@@ -99,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)
@@ -107,11 +135,13 @@ 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.
# GitHub Extra Pins
Yey! you are no longer limited to 6 pinned repositories.
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
@@ -127,9 +157,13 @@ Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`
[![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](#customization) 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)
### 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
You usually won't be able to layout the images side by side. To do that you can use this approach:
```md
<a href="https://github.com/anuraghazra/github-readme-stats">
@@ -140,15 +174,49 @@ You usually won't be able to layout the images side by side to do that you can u
</a>
```
## Deploy on your own vercel instance
## Deploy on your own Vercel instance
Since 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 thats why if you want to host it on your own vercel server then you don't have to worry about anything. click on the deploy button to get started
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
[![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats)
Make sure to add your own `GITHUB_TOKEN` in `Environment Variables`
[Github Docs: Creating a github personal token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token)
<details>
<summary>Guide on setting up Vercel</summary>
1. Go to [vercel.com](https://vercel.com/)
1. Click on `Log in`
![](https://files.catbox.moe/tct1wg.png)
1. Sign in with GitHub by pressing `Continue with GitHub`
![](https://files.catbox.moe/btd78j.jpeg)
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`
![](https://files.catbox.moe/qckos0.png)
1. Select `Import Git Repository`
![](https://files.catbox.moe/pqub9q.png)
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)
![](https://files.catbox.moe/caem5b.png)
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:
- [![paypal.me/anuraghazra](https://ionicabizau.github.io/badges/paypal.svg)](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.
Made with :heart: and JavaScript.
+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 制作。
+60 -19
View File
@@ -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;
+31 -15
View File
@@ -1,14 +1,15 @@
const { request } = require("./utils");
const retryer = require("./retryer");
async function fetchRepo(username, reponame) {
if (!username || !reponame) {
throw new Error("Invalid username or reponame");
}
const res = await request({
query: `
const fetcher = (variables, token) => {
return request(
{
query: `
fragment RepoInfo on Repository {
name
nameWithOwner
isPrivate
isArchived
stargazers {
totalCount
}
@@ -33,11 +34,20 @@ async function fetchRepo(username, reponame) {
}
}
`,
variables: {
login: username,
repo: reponame,
variables,
},
});
{
Authorization: `bearer ${token}`,
}
);
};
async function fetchRepo(username, reponame) {
if (!username || !reponame) {
throw new Error("Invalid username or reponame");
}
let res = await retryer(fetcher, { login: username, repo: reponame });
const data = res.data.data;
@@ -45,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;
+26 -16
View File
@@ -1,32 +1,32 @@
const { request } = require("./utils");
const retryer = require("./retryer");
const calculateRank = require("./calculateRank");
require("dotenv").config();
async function fetchStats(username) {
if (!username) throw Error("Invalid username");
const res = await request({
query: `
const fetcher = (variables, token) => {
return request(
{
query: `
query userInfo($login: String!) {
user(login: $login) {
name
login
repositoriesContributedTo(first: 100, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
totalCount
}
contributionsCollection {
totalCommitContributions
}
pullRequests(first: 100) {
repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
totalCount
}
issues(first: 100) {
pullRequests(first: 1) {
totalCount
}
issues(first: 1) {
totalCount
}
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 {
@@ -36,9 +36,17 @@ async function fetchStats(username) {
}
}
}
`,
variables: { login: username },
});
`,
variables,
},
{
Authorization: `bearer ${token}`,
}
);
};
async function fetchStats(username) {
if (!username) throw Error("Invalid username");
const stats = {
name: "",
@@ -47,12 +55,14 @@ async function fetchStats(username) {
totalIssues: 0,
totalStars: 0,
contributedTo: 0,
rank: "C",
rank: { level: "C", score: 0 },
};
let res = await retryer(fetcher, { login: username });
if (res.data.errors) {
console.log(res.data.errors);
throw Error("Could not fetch user");
throw Error(res.data.errors[0].message || "Could not fetch user");
}
const user = res.data.data.user;
+94
View File
@@ -0,0 +1,94 @@
const calculateCircleProgress = (value) => {
let radius = 40;
let c = Math.PI * (radius * 2);
if (value < 0) value = 0;
if (value > 100) value = 100;
let percentage = ((100 - value) / 100) * c;
return percentage;
};
const getAnimations = ({ progress }) => {
return `
/* Animations */
@keyframes scaleIn {
from {
transform: translate(-5px, 5px) scale(0);
}
to {
transform: translate(-5px, 5px) scale(1);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes rankAnimation {
from {
stroke-dashoffset: ${calculateCircleProgress(0)};
}
to {
stroke-dashoffset: ${calculateCircleProgress(progress)};
}
}
`;
};
const getStyles = ({
titleColor,
textColor,
iconColor,
show_icons,
progress,
}) => {
return `
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor};
animation: fadeIn 0.8s ease-in-out forwards;
}
.stat {
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
}
.stagger {
opacity: 0;
animation: fadeIn 0.3s ease-in-out forwards;
}
.rank-text {
font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor};
animation: scaleIn 0.3s ease-in-out forwards;
}
.bold { font-weight: 700 }
.icon {
fill: ${iconColor};
display: ${!!show_icons ? "block" : "none"};
}
.rank-circle-rim {
stroke: ${titleColor};
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: ${titleColor};
stroke-dasharray: 250;
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
${process.env.NODE_ENV === "test" ? "" : getAnimations({ progress })}
`;
};
module.exports = getStyles;
+11
View File
@@ -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;
+83 -33
View File
@@ -1,64 +1,114 @@
const { kFormatter, encodeHTML, isValidHexColor } = 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";
const height = 120;
const shiftText = primaryLanguage.name.length > 15 ? 0 : 30;
const shiftText = langName.length > 15 ? 0 : 30;
let desc = description || "No description provided";
if (desc.length > 55) {
desc = `${description.slice(0, 55)}..`;
}
const titleColor =
(isValidHexColor(title_color) && `#${title_color}`) || "#2f80ed";
const iconColor =
(isValidHexColor(icon_color) && `#${icon_color}`) || "#586069";
const textColor = (isValidHexColor(text_color) && `#${text_color}`) || "#333";
const bgColor = (isValidHexColor(bg_color) && `#${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="${
primaryLanguage.color
}" />
<text data-testid="lang" class="gray" x="15">${
primaryLanguage.name
}</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>
`;
};
+98 -67
View File
@@ -1,18 +1,25 @@
const { kFormatter, isValidHexColor } = 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);
// manually calculating lineHeight based on index instead of using <tspan dy="" />
// to fix firefox layout bug
const staggerDelay = (index + 3) * 150;
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 x="25" y="${lineHeight * (index + 1)}">
<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>
`;
};
@@ -29,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,
@@ -36,67 +44,97 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
icon_color,
text_color,
bg_color,
theme = "default",
} = options;
const lheight = parseInt(line_height);
const titleColor =
(isValidHexColor(title_color) && `#${title_color}`) || "#2f80ed";
const iconColor =
(isValidHexColor(icon_color) && `#${icon_color}`) || "#4c71f2";
const textColor = (isValidHexColor(text_color) && `#${text_color}`) || "#333";
const bgColor = (isValidHexColor(bg_color) && `#${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 })
// create the text nodes, and pass index so that we can calculate the line spacing
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"
@@ -107,59 +145,52 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
/>
`;
const rankProgress = 180 + rank.score * 0.8;
const rankCircle = hide_rank
? ""
: `<g data-testid="rank-circle" transform="translate(400, ${
height / 1.85
})">
<circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
<circle class="rank-circle" cx="-10" cy="8" r="40" />
<text
x="0"
y="0"
alignment-baseline="central"
dominant-baseline="central"
text-anchor="middle"
class="rank-text"
transform="translate(-5, 5)"
>
${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>`;
if (hide_title) {
height -= 30;
}
return `
<svg width="495" height="${height}" viewBox="0 0 495 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
.header { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor}; }
.stat { font: 600 14px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor}; }
.rank-text { font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor}; }
.star-icon { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; }
.bold { font-weight: 700 }
.icon {
fill: ${iconColor};
display: ${!!show_icons ? "block" : "none"};
}
.rank-circle {
stroke-dashoffset: 30;
stroke-dasharray: ${rankProgress};
stroke: ${titleColor};
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
}
${styles}
</style>
${hide_border ? "" : border}
${rankCircle}
<text x="25" y="35" class="header">${name}'s GitHub Stats</text>
${border}
${title}
<svg x="0" y="45">
${statItems.toString().replace(/\,/gm, "")}
</svg>
<g data-testid="card-body-content" transform="translate(0, ${
hide_title ? -30 : 0
})">
${rankCircle}
<svg x="0" y="55">
${FlexLayout({
items: statItems,
gap: lheight,
direction: "column",
}).join("")}
</svg>
</g>
</svg>
`;
};
+43
View File
@@ -0,0 +1,43 @@
const retryer = async (fetcher, variables, retries = 0) => {
if (retries > 7) {
throw new Error("Maximum retries exceeded");
}
try {
console.log(`Trying PAT_${retries + 1}`);
// try to fetch with the first token since RETRIES is 0 index i'm adding +1
let response = await fetcher(
variables,
process.env[`PAT_${retries + 1}`],
retries
);
// prettier-ignore
const isRateExceeded = response.data.errors && response.data.errors[0].type === "RATE_LIMITED";
// if rate limit is hit increase the RETRIES and recursively call the retryer
// with username, and current RETRIES
if (isRateExceeded) {
console.log(`PAT_${retries + 1} Failed`);
retries++;
// directly return from the function
return retryer(fetcher, variables, retries);
}
// finally return the response
return response;
} catch (err) {
// prettier-ignore
// also checking for bad credentials if any tokens gets invalidated
const isBadCredential = err.response.data && err.response.data.message === "Bad credentials";
if (isBadCredential) {
console.log(`PAT_${retries + 1} Failed`);
retries++;
// directly return from the function
return retryer(fetcher, variables, retries);
}
}
};
module.exports = retryer;
+99 -13
View File
@@ -1,4 +1,5 @@
const axios = require("axios");
const themes = require("../themes");
const renderError = (message) => {
return `
@@ -33,19 +34,104 @@ function isValidHexColor(hexColor) {
).test(hexColor);
}
function request(data) {
return new Promise((resolve, reject) => {
axios({
url: "https://api.github.com/graphql",
method: "post",
headers: {
Authorization: `bearer ${process.env.GITHUB_TOKEN}`,
},
data,
})
.then((response) => resolve(response))
.catch((error) => reject(error));
function parseBoolean(value) {
if (value === "true") {
return true;
} else if (value === "false") {
return false;
} else {
return 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 axios({
url: "https://api.github.com/graphql",
method: "post",
headers,
data,
});
}
module.exports = { renderError, kFormatter, encodeHTML, isValidHexColor, request };
/**
*
* @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,
encodeHTML,
isValidHexColor,
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}`],
]);
}
});
});
+1 -1
View File
@@ -13,6 +13,6 @@ describe("Test calculateRank", () => {
prs: 300,
issues: 200,
})
).toStrictEqual({ level: "S+", score: 192.13 });
).toStrictEqual({ level: "A+", score: 49.16605417270399 });
});
});
+13
View File
@@ -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"
);
});
});
+1 -1
View File
@@ -74,7 +74,7 @@ describe("Test fetchStats", () => {
mock.onPost("https://api.github.com/graphql").reply(200, error);
await expect(fetchStats("anuraghazra")).rejects.toThrow(
"Could not fetch user"
"Could not resolve to a User with the login of 'noname'."
);
});
});
+2
View File
@@ -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 = {
+105 -9
View File
@@ -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();
});
});
+123 -7
View File
@@ -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 = {
@@ -12,7 +17,7 @@ describe("Test renderStatsCard", () => {
totalIssues: 300,
totalPRs: 400,
contributedTo: 500,
rank: { level: "A+", score: 100 },
rank: { level: "A+", score: 40 },
};
it("should render correctly", () => {
@@ -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,7 @@ describe("Test renderStatsCard", () => {
document.body.innerHTML = renderStatsCard(stats);
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const stylesObject = cssToObject(styleTag.textContent);
const headerClassStyles = stylesObject[".header"];
const statClassStyles = stylesObject[".stat"];
@@ -75,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"
);
@@ -101,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");
});
});
+50
View File
@@ -0,0 +1,50 @@
require("@testing-library/jest-dom");
const retryer = require("../src/retryer");
const fetcher = jest.fn((variables, token) => {
console.log(variables, token);
return new Promise((res, rej) => res({ data: "ok" }));
});
const fetcherFail = jest.fn(() => {
return new Promise((res, rej) =>
res({ data: { errors: [{ type: "RATE_LIMITED" }] } })
);
});
const fetcherFailOnSecondTry = jest.fn((_vars, _token, retries) => {
return new Promise((res, rej) => {
// faking rate limit
if (retries < 1) {
return res({ data: { errors: [{ type: "RATE_LIMITED" }] } });
}
return res({ data: "ok" });
});
});
describe("Test Retryer", () => {
it("retryer should return value and have zero retries on first try", async () => {
let res = await retryer(fetcher, {});
expect(fetcher).toBeCalledTimes(1);
expect(res).toStrictEqual({ data: "ok" });
});
it("retryer should return value and have 2 retries", async () => {
let res = await retryer(fetcherFailOnSecondTry, {});
expect(fetcherFailOnSecondTry).toBeCalledTimes(2);
expect(res).toStrictEqual({ data: "ok" });
});
it("retryer should throw error if maximum retries reached", async () => {
let res;
try {
res = await retryer(fetcherFail, {});
} catch (err) {
expect(fetcherFail).toBeCalledTimes(8);
expect(err.message).toBe("Maximum retries exceeded");
}
});
});
+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;