Compare commits

...
Author SHA1 Message Date
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
23 changed files with 834 additions and 215 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.
+8 -4
View File
@@ -1,5 +1,5 @@
require("dotenv").config();
const { renderError } = require("../src/utils");
const { renderError, parseBoolean } = require("../src/utils");
const fetchStats = require("../src/fetchStats");
const renderStatsCard = require("../src/renderStatsCard");
@@ -7,6 +7,7 @@ module.exports = async (req, res) => {
const {
username,
hide,
hide_title,
hide_border,
hide_rank,
show_icons,
@@ -18,7 +19,9 @@ module.exports = async (req, res) => {
} = req.query;
let stats;
res.setHeader("Cache-Control", "public, max-age=1800");
res.setHeader("Content-Type", "image/svg+xml");
try {
stats = await fetchStats(username);
} catch (err) {
@@ -28,9 +31,10 @@ module.exports = async (req, res) => {
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,
+5 -1
View File
@@ -1,5 +1,5 @@
require("dotenv").config();
const { renderError } = require("../src/utils");
const { renderError, parseBoolean } = require("../src/utils");
const fetchRepo = require("../src/fetchRepo");
const renderRepoCard = require("../src/renderRepoCard");
@@ -11,9 +11,12 @@ module.exports = async (req, res) => {
icon_color,
text_color,
bg_color,
show_owner,
} = req.query;
let repoData;
res.setHeader("Cache-Control", "public, max-age=1800");
res.setHeader("Content-Type", "image/svg+xml");
try {
@@ -29,6 +32,7 @@ module.exports = async (req, res) => {
icon_color,
text_color,
bg_color,
show_owner: parseBoolean(show_owner),
})
);
};
+71 -30
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">
@@ -22,29 +24,30 @@
<a href="https://github.com/anuraghazra/github-readme-stats/issues">Request Feature</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)
- [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"]`
@@ -54,30 +57,30 @@ 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:
- `&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
### 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 |
- You can also customize the cards to be compatible with dark mode
@@ -107,11 +110,11 @@ 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
Github extra pins allow you to pin more than 6 repositories in your profile using a GitHub readme profile.
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.
Yey! You are no longer limited to 6 pinned repositories.
### Usage
@@ -127,9 +130,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 +147,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.
+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, 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;
+71 -32
View File
@@ -1,64 +1,103 @@
const { kFormatter, encodeHTML, isValidHexColor } = require("../src/utils");
const {
kFormatter,
encodeHTML,
fallbackColor,
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 } = 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";
const titleColor = fallbackColor(title_color, "#2f80ed");
const iconColor = fallbackColor(icon_color, "#586069");
const textColor = fallbackColor(text_color, "#333");
const bgColor = fallbackColor(bg_color, "#FFFEFE");
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 =
totalForks > 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"/>
<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>
`;
};
+105 -76
View File
@@ -1,12 +1,25 @@
const { kFormatter, isValidHexColor } = require("../src/utils");
const { kFormatter, fallbackColor, FlexLayout } = require("../src/utils");
const getStyles = require("./getStyles");
const icons = require("./icons");
const createTextNode = ({ icon, label, value, lineHeight, id }) => {
const classname = icon === "★" && "star-icon";
const createTextNode = ({ icon, label, value, id, index, showIcons }) => {
const kValue = kFormatter(value);
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 `
<tspan x="25" dy="${lineHeight}" class="stat bold">
<tspan data-testid="icon" class="icon ${classname}">${icon}</tspan> ${label}:</tspan>
<tspan data-testid="${id}" x="155" dy="0" class="stat">${kValue}</tspan>
<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>
`;
};
@@ -23,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,
@@ -34,63 +48,84 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
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";
const titleColor = fallbackColor(title_color, "#2f80ed");
const iconColor = fallbackColor(icon_color, "#4c71f2");
const textColor = fallbackColor(text_color, "#333");
const bgColor = fallbackColor(bg_color, "#FFFEFE");
const STAT_MAP = {
stars: createTextNode({
icon: "★",
// Meta data for creating text nodes with createTextNode function
const STATS = {
stars: {
icon: icons.star,
label: "Total Stars",
value: totalStars,
lineHeight: lheight,
id: "stars",
}),
commits: createTextNode({
icon: "🕗",
},
commits: {
icon: icons.commits,
label: "Total Commits",
value: totalCommits,
lineHeight: lheight,
id: "commits",
}),
prs: createTextNode({
icon: "🔀",
},
prs: {
icon: icons.prs,
label: "Total PRs",
value: totalPRs,
lineHeight: lheight,
id: "prs",
}),
issues: createTextNode({
icon: "ⓘ",
},
issues: {
icon: icons.issues,
label: "Total Issues",
value: totalIssues,
lineHeight: lheight,
id: "issues",
}),
contribs: createTextNode({
icon: "📕",
},
contribs: {
icon: icons.contribs,
label: "Contributed to",
value: contributedTo,
lineHeight: lheight,
id: "contribs",
}),
},
};
const statItems = Object.keys(STAT_MAP)
// filter out hidden stats defined by user & create the text nodes
const statItems = Object.keys(STATS)
.filter((key) => !hide.includes(key))
.map((key) => STAT_MAP[key]);
.map((key, index) =>
// 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 title = hide_title
? ""
: `<text x="25" y="35" class="header">${name}'s GitHub Stats</text>`;
const border = hide_border
? ""
: `
<rect
data-testid="card-border"
x="0.5"
@@ -103,58 +138,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>
<text y="45">
${statItems.toString().replace(/\,/gm, "")}
</text>
${border}
${title}
<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;
+51 -13
View File
@@ -33,19 +33,57 @@ 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 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>`;
});
}
module.exports = {
renderError,
kFormatter,
encodeHTML,
isValidHexColor,
request,
parseBoolean,
fallbackColor,
FlexLayout,
};
+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 = {
+53 -3
View File
@@ -6,6 +6,7 @@ const { queryByTestId } = require("@testing-library/dom");
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 +23,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 +41,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,
@@ -136,4 +147,43 @@ describe("Test renderRepoCard", () => {
"#252525"
);
});
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();
});
});
+59 -3
View File
@@ -2,7 +2,11 @@ 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");
describe("Test renderStatsCard", () => {
const stats = {
@@ -12,7 +16,7 @@ describe("Test renderStatsCard", () => {
totalIssues: 300,
totalPRs: 400,
contributedTo: 500,
rank: { level: "A+", score: 100 },
rank: { level: "A+", score: 40 },
};
it("should render correctly", () => {
@@ -66,7 +70,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"];
@@ -106,4 +110,56 @@ describe("Test renderStatsCard", () => {
"#252525"
);
});
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");
}
});
});
+27 -1
View File
@@ -1,4 +1,9 @@
const { kFormatter, encodeHTML, renderError } = require("../src/utils");
const {
kFormatter,
encodeHTML,
renderError,
FlexLayout,
} = require("../src/utils");
describe("Test utils.js", () => {
it("should test kFormatter", () => {
@@ -23,4 +28,25 @@ 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>`
);
});
});