Compare commits

..
Author SHA1 Message Date
rickstaa 6883fec5c7 Merge branch 'master' into theme_preview_batch 2022-10-02 17:43:51 +02:00
rickstaa 90f1ed40f9 ci: add batch theme preview script 2022-10-02 17:43:48 +02:00
43 changed files with 991 additions and 2106 deletions
+43
View File
@@ -0,0 +1,43 @@
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: ""
assignees: ""
---
<!--PLEASE FIRST READ THE FAQ (#1770) AND COMMON ERROR CODES (#1772)!!!-->
**Describe the bug**
A clear and concise description of what the bug is.
**Expected behaviour**
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.
<!-- FAQ
Below are some questions that are found in the FAQ. The full FAQ can be found in #1770.
Q: My card displays an error
Ans: First, check the common error codes (i.e. https://github.com/anuraghazra/github-readme-stats/issues/1772) and existing issues before creating a new one.
Q: How to hide jupyter Notebook?
Ans: &hide=jupyter%20notebook
Q: I could not figure out how to deploy on my own vercel instance
Ans:
- docs: https://github.com/anuraghazra/github-readme-stats/#deploy-on-your-own-vercel-instance
- YT tutorial by codeSTACKr: https://www.youtube.com/watch?v=n6d4KHSKqGk&feature=youtu.be&t=107
Q: Language Card is incorrect
Ans: Please read all the issues/comments before opening any issues regarding language card stats:
- https://github.com/anuraghazra/github-readme-stats/issues/136#issuecomment-665164174
- https://github.com/anuraghazra/github-readme-stats/issues/136#issuecomment-665172181
Q: How to count private stats?
Ans: We can only count private commits & we cannot access any other private info of any users, so it's impossible. The only way is to deploy on your own instance & use your own PAT (Personal Access Token)
-->
-58
View File
@@ -1,58 +0,0 @@
name: Bug report
description: Create a report to help us improve.
labels:
- "bug"
body:
- type: markdown
attributes:
value: |
:warning: PLEASE FIRST READ THE FAQ [(#1770)](https://github.com/anuraghazra/github-readme-stats/discussions/1770) AND COMMON ERROR CODES [(#1772)](https://github.com/anuraghazra/github-readme-stats/issues/1772)!!!
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
- type: textarea
attributes:
label: Expected behaviour
description:
A clear and concise description of what you expected to happen.
- type: textarea
attributes:
label: Screenshots / Live demo link
description: If applicable, add screenshots to help explain your problem.
placeholder: Paste the github-readme-stats link as markdown image
- type: textarea
attributes:
label: Additional context
description: Add any other context about the problem here.
- type: markdown
attributes:
value: |
---
### FAQ (Snippet)
Below are some questions that are found in the FAQ. The full FAQ can be found in [#1770](https://github.com/anuraghazra/github-readme-stats/discussions/1770).
#### Q: My card displays an error
**Ans:** First, check the common error codes (i.e. https://github.com/anuraghazra/github-readme-stats/issues/1772) and existing issues before creating a new one.
#### Q: How to hide jupyter Notebook?
**Ans:** `&hide=jupyter%20notebook`.
#### Q: I could not figure out how to deploy on my own vercel instance
**Ans:** Please check:
- Docs: https://github.com/anuraghazra/github-readme-stats/#deploy-on-your-own-vercel-instance
- YT tutorial by codeSTACKr: https://www.youtube.com/watch?v=n6d4KHSKqGk&feature=youtu.be&t=107
#### Q: Language Card is incorrect
**Ans:** Please read these issues/comments before opening any issues regarding language card stats:
- https://github.com/anuraghazra/github-readme-stats/issues/136#issuecomment-665164174
- https://github.com/anuraghazra/github-readme-stats/issues/136#issuecomment-665172181
#### Q: How to count private stats?
**Ans:** We can only count private commits & we cannot access any other private info of any users, so it's impossible. The only way is to deploy on your own instance & use your own PAT (Personal Access Token).
-12
View File
@@ -1,12 +0,0 @@
blank_issues_enabled: true
contact_links:
- name: Question
url: https://github.com/anuraghazra/github-readme-stats/discussions
about: Please ask and answer questions here.
- name: Error
url: https://github.com/anuraghazra/github-readme-stats/issues/1772
about:
Before opening a bug report, please check the 'Common Error Codes' issue.
- name: FAQ
url: https://github.com/anuraghazra/github-readme-stats/discussions/1770
about: Please first check the FAQ before asking a question.
+41
View File
@@ -0,0 +1,41 @@
---
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.
<!--
PLEASE READ THE FAQs!!!
Q: How to hide jupyter Notebook?
Ans: &hide=jupyter%20notebook
Q: I could not figure out how to deploy on my own vercel instance
Ans:
- docs: https://github.com/anuraghazra/github-readme-stats/#deploy-on-your-own-vercel-instance
- YT tutorial by codeSTACKr: https://www.youtube.com/watch?v=n6d4KHSKqGk&feature=youtu.be&t=107
Q: Language Card is incorrect
Ans: Please read all the issues / comments before opening any issues regarding language card stats:
- https://github.com/anuraghazra/github-readme-stats/issues/136#issuecomment-665164174
- https://github.com/anuraghazra/github-readme-stats/issues/136#issuecomment-665172181
Q: How to count private stats?
Ans: We can only count private commits & we cannot access any other private info of any users, so it's not possible. only way is to deploy on your own instance & use your own PAT (Personal Access Token)
-->
@@ -1,26 +0,0 @@
name: Feature request
description: Suggest an idea for this project.
labels:
- "enhancement"
body:
- type: textarea
attributes:
label: Is your feature request related to a problem? Please describe.
description:
A clear and concise description of what the problem is. Ex. I'm always
frustrated when [...]
- type: textarea
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
- type: textarea
attributes:
label: Describe alternatives you've considered
description:
A clear and concise description of any alternative solutions or features
you've considered.
- type: textarea
attributes:
label: Additional context
description:
Add any other context or screenshots about the feature request here.
-1
View File
@@ -1 +0,0 @@
_
-5
View File
@@ -1,5 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm test
npx lint-staged
-1
View File
@@ -89,7 +89,6 @@ export default async (req, res) => {
}), }),
); );
} catch (err) { } catch (err) {
res.setHeader("Cache-Control", `no-store`); // Don't cache error responses.
return res.send(renderError(err.message, err.secondaryMessage)); return res.send(renderError(err.message, err.secondaryMessage));
} }
}; };
-1
View File
@@ -75,7 +75,6 @@ export default async (req, res) => {
}), }),
); );
} catch (err) { } catch (err) {
res.setHeader("Cache-Control", `no-store`); // Don't cache error responses.
return res.send(renderError(err.message, err.secondaryMessage)); return res.send(renderError(err.message, err.secondaryMessage));
} }
}; };
-1
View File
@@ -76,7 +76,6 @@ export default async (req, res) => {
}), }),
); );
} catch (err) { } catch (err) {
res.setHeader("Cache-Control", `no-store`); // Don't cache error responses.
return res.send(renderError(err.message, err.secondaryMessage)); return res.send(renderError(err.message, err.secondaryMessage));
} }
}; };
-1
View File
@@ -78,7 +78,6 @@ export default async (req, res) => {
}), }),
); );
} catch (err) { } catch (err) {
res.setHeader("Cache-Control", `no-store`); // Don't cache error responses.
return res.send(renderError(err.message, err.secondaryMessage)); return res.send(renderError(err.message, err.secondaryMessage));
} }
}; };
+383 -876
View File
File diff suppressed because it is too large Load Diff
+9 -23
View File
@@ -1,23 +1,9 @@
{ {
"name": "github-readme-stats", "name": "github-readme-stats",
"version": "1.0.0", "version": "1.0.0",
"description": "Dynamically generate stats for your GitHub readme", "description": "Dynamically generate stats for your github readmes",
"keywords": [ "main": "index.js",
"github-readme-stats",
"readme-stats",
"cards",
"card-generator"
],
"main": "src/index.js",
"type": "module", "type": "module",
"homepage": "https://github.com/anuraghazra/github-readme-stats",
"bugs": {
"url": "https://github.com/anuraghazra/github-readme-stats/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/anuraghazra/github-readme-stats.git"
},
"scripts": { "scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage", "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch", "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
@@ -27,9 +13,8 @@
"preview-theme": "node scripts/preview-theme", "preview-theme": "node scripts/preview-theme",
"close-stale-theme-prs": "node scripts/close-stale-theme-prs", "close-stale-theme-prs": "node scripts/close-stale-theme-prs",
"generate-langs-json": "node scripts/generate-langs-json", "generate-langs-json": "node scripts/generate-langs-json",
"format": "prettier --write .", "format": "./node_modules/.bin/prettier --write .",
"format:check": "prettier --check .", "format:check": "./node_modules/.bin/prettier --check ."
"prepare": "husky install"
}, },
"author": "Anurag Hazra", "author": "Anurag Hazra",
"license": "MIT", "license": "MIT",
@@ -42,11 +27,10 @@
"axios-mock-adapter": "^1.18.1", "axios-mock-adapter": "^1.18.1",
"color-contrast-checker": "^2.1.0", "color-contrast-checker": "^2.1.0",
"hjson": "^3.2.2", "hjson": "^3.2.2",
"husky": "^8.0.0", "husky": "^4.2.5",
"jest": "^29.0.3", "jest": "^29.0.3",
"jest-environment-jsdom": "^29.0.3", "jest-environment-jsdom": "^29.0.3",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lint-staged": "^13.0.3",
"lodash.snakecase": "^4.1.1", "lodash.snakecase": "^4.1.1",
"parse-diff": "^0.7.0", "parse-diff": "^0.7.0",
"prettier": "^2.1.2" "prettier": "^2.1.2"
@@ -59,7 +43,9 @@
"upgrade": "^1.1.0", "upgrade": "^1.1.0",
"word-wrap": "^1.2.3" "word-wrap": "^1.2.3"
}, },
"lint-staged": { "husky": {
"*.{js,css,md}": "prettier --write" "hooks": {
"pre-commit": "npm test"
}
} }
} }
+135 -143
View File
@@ -7,9 +7,6 @@
<a href="https://github.com/anuraghazra/github-readme-stats/actions"> <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" /> <img alt="Tests Passing" src="https://github.com/anuraghazra/github-readme-stats/workflows/Test/badge.svg" />
</a> </a>
<a href="https://github.com/anuraghazra/github-readme-stats/graphs/contributors">
<img alt="GitHub Contributors" src="https://img.shields.io/github/contributors/anuraghazra/github-readme-stats" />
</a>
<a href="https://codecov.io/gh/anuraghazra/github-readme-stats"> <a href="https://codecov.io/gh/anuraghazra/github-readme-stats">
<img src="https://codecov.io/gh/anuraghazra/github-readme-stats/branch/master/graph/badge.svg" /> <img src="https://codecov.io/gh/anuraghazra/github-readme-stats/branch/master/graph/badge.svg" />
</a> </a>
@@ -62,41 +59,42 @@
<a href="/docs/readme_tr.md">Türkçe</a> <a href="/docs/readme_tr.md">Türkçe</a>
</p> </p>
</p> </p>
<p align="center">Love the project? Please consider <a href="https://www.paypal.me/anuraghazra">donating</a> to help it improve!
<p align="center">Love the project? Please consider <a href="https://www.paypal.me/anuraghazra">donating</a> to help it improve!</p>
<p>
<a href="https://indiafightscorona.giveindia.org"> <a href="https://indiafightscorona.giveindia.org">
<img src="https://indiaspora.org/wp-content/uploads/2021/04/give-India-logo.png" alt="Give india logo" width="200" /> <img src="https://d2wvdrxmr8p0wf.cloudfront.net/static/giveindia.svg" alt="Give india logo" width="200" />
</a> </a>
Are you considering supporting the project by donating? Please DO NOT!! Are you considering supporting the project by donating? Please DON'T!!
Instead, Help India fight the second deadly wave of COVID-19. Instead, Help India fight the 2nd deadly wave of COVID-19.
Thousands of people are dying in India because of a lack of Oxygen & also COVID-related infrastructure. Thousands of people are dying in India because of a lack of Oxygen & also COVID-related infrastructure.
Visit <https://indiafightscorona.giveindia.org> and make a small donation to help us fight COVID and overcome this crisis. A small donation goes a long way. :heart: Visit [https://indiafightscorona.giveindia.org](https://indiafightscorona.giveindia.org) and make a small donation to help us fight COVID and overcome this crisis.
A small donation goes a long way. :heart:
</p> </p>
# Features # Features
- [GitHub Stats Card](#github-stats-card) - [GitHub Stats Card](#github-stats-card)
- [GitHub Extra Pins](#github-extra-pins) - [GitHub Extra Pins](#github-extra-pins)
- [Top Languages Card](#top-languages-card) - [Top Languages Card](#top-languages-card)
- [Wakatime Week Stats](#wakatime-week-stats) - [Wakatime Week Stats](#wakatime-week-stats)
- [Themes](#themes) - [Themes](#themes)
- [Customization](#customization) - [Customization](#customization)
- [Common Options](#common-options) - [Common Options](#common-options)
- [Stats Card Exclusive Options](#stats-card-exclusive-options) - [Stats Card Exclusive Options](#stats-card-exclusive-options)
- [Repo Card Exclusive Options](#repo-card-exclusive-options) - [Repo Card Exclusive Options](#repo-card-exclusive-options)
- [Language Card Exclusive Options](#language-card-exclusive-options) - [Language Card Exclusive Options](#language-card-exclusive-options)
- [Wakatime Card Exclusive Option](#wakatime-card-exclusive-options) - [Wakatime Card Exclusive Option](#wakatime-card-exclusive-options)
- [Deploy Yourself](#deploy-on-your-own-vercel-instance) - [Deploy Yourself](#deploy-on-your-own-vercel-instance)
- [Keep your fork up to date](#keep-your-fork-up-to-date)
# GitHub Stats Card # GitHub Stats Card
Copy-paste this into your markdown content, and that is it. Simple! Copy-paste this into your markdown content, and that's it. Simple!
Change the `?username=` value to your GitHub username. Change the `?username=` value to your GitHub username.
@@ -104,12 +102,13 @@ Change the `?username=` value to your GitHub username.
[![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats) [![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats)
``` ```
> **Note** _Note: Available ranks are S+ (top 1%), S (top 25%), A++ (top 45%), A+ (top 60%), and B+ (everyone).
> Available ranks are S+ (top 1%), S (top 25%), A++ (top 45%), A+ (top 60%), and B+ (everyone). The values are calculated by using the [cumulative distribution function](https://en.wikipedia.org/wiki/Cumulative_distribution_function) using commits, contributions, issues, stars, pull requests, followers, and owned repositories. The implementation can be investigated at [src/calculateRank.js](./src/calculateRank.js). The values are calculated by using the [cumulative distribution function](https://en.wikipedia.org/wiki/Cumulative_distribution_function) using commits, contributions, issues, stars, pull requests, followers, and owned repositories.
The implementation can be investigated at [src/calculateRank.js](./src/calculateRank.js)._
### Hiding individual stats ### Hiding individual stats
You can pass a query parameter `&hide=` to hide any specific stats with comma-separated values. To hide any specific stats, you can pass a query parameter `&hide=` with comma-separated values.
> Options: `&hide=stars,commits,prs,issues,contribs` > Options: `&hide=stars,commits,prs,issues,contribs`
@@ -121,8 +120,7 @@ You can pass a query parameter `&hide=` to hide any specific stats with comma-se
You can add the count of all your private contributions to the total commits count by using the query parameter `&count_private=true`. You can add the count of all your private contributions to the total commits count by using the query parameter `&count_private=true`.
> **Note** _Note: If you are deploying this project yourself, the private contributions will be counted by default. Otherwise, you need to choose to share your private contribution counts._
> If you are deploying this project yourself, the private contributions will be counted by default. If you are using the public Vercel instance, you need to choose to [share your private contributions](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-github-profile/managing-contribution-settings-on-your-profile/showing-your-private-contributions-and-achievements-on-your-profile).
> Options: `&count_private=true` > Options: `&count_private=true`
@@ -142,15 +140,15 @@ To enable icons, you can pass `show_icons=true` in the query param, like so:
With inbuilt themes, you can customize the look of the card without doing any [manual customization](#customization). With inbuilt themes, you can customize the look of the card without doing any [manual customization](#customization).
Use `&theme=THEME_NAME` parameter like so : Use `&theme=THEME_NAME` parameter like so :-
```md ```md
![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=radical) ![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=radical)
``` ```
#### All inbuilt themes #### All inbuilt themes:-
Github readme stats comes with several built-in themes (e.g. `dark`, `radical`, `merko`, `gruvbox`, `tokyonight`, `onedark`, `cobalt`, `synthwave`, `highcontrast`, `dracula`). 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 Stats Themes" width="600px"/> <img src="https://res.cloudinary.com/anuraghazra/image/upload/v1595174536/grs-themes_l4ynja.png" alt="GitHub Readme Stats Themes" width="600px"/>
@@ -158,84 +156,87 @@ You can look at a preview for [all available themes](./themes/README.md) or chec
### Customization ### Customization
You can customize the appearance of your `Stats Card` or `Repo Card` however you wish with URL parameters. You can customize the appearance of your `Stats Card` or `Repo Card` however you wish with URL params.
#### Common Options #### Common Options:
- `title_color` - Card's title color _(hex color)_. Default: `2f80ed`. - `title_color` - Card's title color _(hex color)_. Default: `2f80ed`.
- `text_color` - Body text color _(hex color)_. Default: `434d58`. - `text_color` - Body text color _(hex color)_. Default: `434d58`.
- `icon_color` - Icons color if available _(hex color)_. Default: `4c71f2`. - `icon_color` - Icons color if available _(hex color)_. Default: `4c71f2`.
- `border_color` - Card's border color _(hex color)_. Default: `e4e2e2` (Does not apply when `hide_border` is enabled). - `border_color` - Card's border color _(hex color)_. Default: `e4e2e2` (Does not apply when `hide_border` is enabled).
- `bg_color` - Card's background color _(hex color)_ **or** a gradient in the form of _angle,start,end_. Default: `fffefe` - `bg_color` - Card's background color _(hex color)_ **or** a gradient in the form of _angle,start,end_. Default: `fffefe`
- `hide_border` - Hides the card's border _(boolean)_. Default: `false` - `hide_border` - Hides the card's border _(boolean)_. Default: `false`
- `theme` - name of the theme, choose from [all available themes](./themes/README.md). Default: `default` theme. - `theme` - name of the theme, choose from [all available themes](./themes/README.md). Default: `default` theme.
- `cache_seconds` - set the cache header manually _(min: 7200, max: 86400)_. Default: `14400 seconds (4 hours)`. - `cache_seconds` - set the cache header manually _(min: 7200, max: 86400)_. Default: `14400 seconds (4 hours)`.
- `locale` - set the language in the card _(e.g. cn, de, es, etc.)_. Default: `en`. - `locale` - set the language in the card _(e.g. cn, de, es, etc.)_. Default: `en`.
- `border_radius` - Corner rounding on the card. Default: `4.5`. - `border_radius` - Corner rounding on the card. Default: `4.5`.
> **Warning** > Note: The minimum of cache_seconds is currently 4 hours as a temporary fix for PATs exhaustion.
> We use caching to decrease the load on our servers (see https://github.com/anuraghazra/github-readme-stats/issues/1471#issuecomment-1271551425). Our cards have a default cache of 4 hours (14400 seconds). Also, note that the cache is clamped to a minimum of 4 hours and a maximum of 24 hours.
##### Gradient in bg_color ##### Gradient in bg_color
You can provide multiple comma-separated values in the bg_color option to render a gradient with the following format: You can provide multiple comma-separated values in the bg_color option to render a gradient, with the following format:
&bg_color=DEG,COLOR1,COLOR2,COLOR3...COLOR10 ```
&bg_color=DEG,COLOR1,COLOR2,COLOR3...COLOR10
```
#### Stats Card Exclusive Options > Note on cache: Repo cards have a default cache of 4 hours (14400 seconds) if the fork count & star count is less than 1k, otherwise, it's 2 hours (7200 seconds). Also, note that the cache is clamped to a minimum of 2 hours and a maximum of 24 hours.
- `hide` - Hides the [specified items](#hiding-individual-stats) from stats _(Comma-separated values)_. Default: `[] (blank array)`. #### Stats Card Exclusive Options:
- `hide_title` - _(boolean)_. Default: `false`.
- `card_width` - Set the card's width manually _(number)_. Default: `500px (approx.)`.
- `hide_rank` - _(boolean)_ hides the rank and automatically resizes the card width. Default: `false`.
- `show_icons` - _(boolean)_. Default: `false`.
- `include_all_commits` - Count total commits instead of just the current year commits _(boolean)_. Default: `false`.
- `count_private` - Count private commits _(boolean)_. Default: `false`.
- `line_height` - Sets the line height between text _(number)_. Default: `25`.
- `exclude_repo` - Exclude stars from specified repositories _(Comma-separated values)_. Default: `[] (blank array)`.
- `custom_title` - Sets a custom title for the card. Default: `<username> Github Stats`.
- `text_bold` - Use bold text _(boolean)_. Default: `true`.
- `disable_animations` - Disables all animations in the card _(boolean)_. Default: `false`.
> **Note** - `hide` - Hides the [specified items](#hiding-individual-stats) from stats _(Comma-separated values)_. Default: `[] (blank array)`.
> When hide_rank=`true`, the minimum card width is 270 px + the title length and padding. - `hide_title` - _(boolean)_. Default: `false`.
- `card_width` - Set the card's width manually _(number)_. Default: `500px (approx.)`.
- `hide_rank` - _(boolean)_ hides the rank and automatically resizes the card width. Default: `false`.
- `show_icons` - _(boolean)_. Default: `false`.
- `include_all_commits` - Count total commits instead of just the current year commits _(boolean)_. Default: `false`.
- `count_private` - Count private commits _(boolean)_. Default: `false`.
- `line_height` - Sets the line-height between text _(number)_. Default: `25`.
- `exclude_repo` - Exclude stars from specified repositories _(Comma-separated values)_. Default: `[] (blank array)`.
- `custom_title` - Sets a custom title for the card. Default: `<username> Github Stats`.
- `text_bold` - Use bold text _(boolean)_. Default: `true`.
- `disable_animations` - Disables all animations in the card _(boolean)_. Default: `false`.
#### Repo Card Exclusive Options > Note on `hide_rank`:
When hide_rank=`true`, the minimum card width is 270 px + the title length and padding.
- `show_owner` - Show the repo's owner name _(boolean)_. Default: `false`. #### Repo Card Exclusive Options:
#### Language Card Exclusive Options - `show_owner` - Show the repo's owner name _(boolean)_
- `hide` - Hide the languages specified from the card _(Comma-separated values)_. Default: `[] (blank array)`. #### Language Card Exclusive Options:
- `hide_title` - _(boolean)_. Default: `false`.
- `layout` - Switch between two available layouts `default` & `compact`. Default: `default`.
- `card_width` - Set the card's width manually _(number)_. Default `300`.
- `langs_count` - Show more languages on the card, between 1-10 _(number)_. Default `5`.
- `exclude_repo` - Exclude specified repositories _(Comma-separated values)_. Default: `[] (blank array)`.
- `custom_title` - Sets a custom title for the card _(string)_. Default `Most Used Languages`.
> **Warning** - `hide` - Hide the languages specified from the card _(Comma-separated values)_
> Language names should be URI-escaped, as specified in [Percent Encoding](https://en.wikipedia.org/wiki/Percent-encoding) - `hide_title` - _(boolean)_
- `layout` - Switch between two available layouts `default` & `compact`
- `card_width` - Set the card's width manually _(number)_
- `langs_count` - Show more languages on the card, between 1-10, defaults to 5 _(number)_
- `exclude_repo` - Exclude specified repositories _(Comma-separated values)_
- `custom_title` - Sets a custom title for the card
> :warning: **Important:**
> Language names should be uri-escaped, as specified in [Percent Encoding](https://en.wikipedia.org/wiki/Percent-encoding)
> (i.e: `c++` should become `c%2B%2B`, `jupyter notebook` should become `jupyter%20notebook`, etc.) You can use > (i.e: `c++` should become `c%2B%2B`, `jupyter notebook` should become `jupyter%20notebook`, etc.) You can use
> [urlencoder.org](https://www.urlencoder.org/) to help you do this automatically. > [urlencoder.org](https://www.urlencoder.org/) to help you do this automatically.
#### Wakatime Card Exclusive Options #### Wakatime Card Exclusive Options:
- `hide` - Hide the languages specified from the card _(Comma-separated values)_. Default: `[] (blank array)`. - `hide` - Hide the languages specified from the card _(Comma-separated values)_
- `hide_title` - _(boolean)_. Default `false`. - `hide_title` - _(boolean)_
- `line_height` - Sets the line height between text _(number)_. Default `25`. - `line_height` - Sets the line-height between text _(number)_
- `hide_progress` - Hides the progress bar and percentage _(boolean)_. Default `false`. - `hide_progress` - Hides the progress bar and percentage _(boolean)_
- `custom_title` - Sets a custom title for the card _(string)_. Default `Wakatime Stats`. - `custom_title` - Sets a custom title for the card
- `layout` - Switch between two available layouts `default` & `compact`. Default `default`. - `layout` - Switch between two available layouts `default` & `compact`
- `langs_count` - Limit the number of languages on the card, defaults to all reported languages _(number)_. - `langs_count` - Limit the number of languages on the card, defaults to all reported languages
- `api_domain` - Set a custom API domain for the card, e.g. to use services like [Hakatime](https://github.com/mujx/hakatime) or [Wakapi](https://github.com/muety/wakapi) _(string)_. Default `Waka API`. - `api_domain` - Set a custom API domain for the card, e.g. to use services like [Hakatime](https://github.com/mujx/hakatime) or [Wakapi](https://github.com/muety/wakapi)
- `range` Request a range different from your WakaTime default, e.g. `last_7_days`. See [WakaTime API docs](https://wakatime.com/developers#stats) for a list of available options. _(YYYY-MM, last_7_days, last_30_days, last_6_months, last_year, or all_time)_. Default `all_time`. - `range` Request a range different from your WakaTime default, e.g. `last_7_days`. See [WakaTime API docs](https://wakatime.com/developers#stats) for a list of available options.
* * * ---
# GitHub Extra Pins # GitHub Extra Pins
GitHub extra pins allow you to pin more than six 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.
Yay! You are no longer limited to 6 pinned repositories. Yay! You are no longer limited to 6 pinned repositories.
@@ -261,8 +262,7 @@ Use [show_owner](#customization) variable to include the repo's owner username
The top languages card shows a GitHub user's most frequently used top language. The top languages card shows a GitHub user's most frequently used top language.
> **Note** _NOTE: Top Languages does not indicate my skill level or anything like that; it's a GitHub metric to determine which languages have the most code on GitHub. It's a new feature of github-readme-stats._
> Top Languages does not indicate my skill level or anything like that; it's a GitHub metric to determine which languages have the most code on GitHub. It is a new feature of github-readme-stats._
### Usage ### Usage
@@ -276,7 +276,7 @@ Endpoint: `api/top-langs?username=anuraghazra`
### Exclude individual repositories ### Exclude individual repositories
You can use the `&exclude_repo=repo1,repo2` parameter to exclude individual repositories. You can use `&exclude_repo=repo1,repo2` parameter to exclude individual repositories.
```md ```md
[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&exclude_repo=github-readme-stats,anuraghazra.github.io)](https://github.com/anuraghazra/github-readme-stats) [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&exclude_repo=github-readme-stats,anuraghazra.github.io)](https://github.com/anuraghazra/github-readme-stats)
@@ -310,7 +310,7 @@ You can use the `&layout=compact` option to change the card design.
[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats) [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats)
- Compact layout - Compact layout
[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=compact)](https://github.com/anuraghazra/github-readme-stats) [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=compact)](https://github.com/anuraghazra/github-readme-stats)
@@ -322,8 +322,7 @@ Change the `?username=` value to your [Wakatime](https://wakatime.com) username.
[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) [![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats)
``` ```
> **Note**: > Note: Please be aware that we currently only show data from Wakatime profiles that are public.
> Please be aware that we currently only show data from Wakatime profiles that are public.
### Demo ### Demo
@@ -331,69 +330,69 @@ Change the `?username=` value to your [Wakatime](https://wakatime.com) username.
[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats) [![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats)
- Compact layout - Compact layout
[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod&layout=compact)](https://github.com/anuraghazra/github-readme-stats) [![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod&layout=compact)](https://github.com/anuraghazra/github-readme-stats)
* * * ---
### All Demos ### All Demos
- Default - Default
![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)
- Hiding specific stats - Hiding specific stats
![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=contribs,issues)
- Showing icons - Showing icons
![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&hide=issues&show_icons=true)
- Customize Border Color - Customize Border Color
![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&border_color=2e4058) ![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&border_color=2e4058)
- Include All Commits - Include All Commits
![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&include_all_commits=true) ![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&include_all_commits=true)
- Themes - Themes
Choose from any of the [default themes](#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) ![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=radical)
- Gradient - Gradient
![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&bg_color=30,e96443,904e95&title_color=fff&text_color=fff) ![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&bg_color=30,e96443,904e95&title_color=fff&text_color=fff)
- Customizing stats card - 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) ![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)
- Setting card locale - Setting card locale
![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api/?username=anuraghazra&locale=es) ![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api/?username=anuraghazra&locale=es)
- Customizing repo card - Customizing repo card
![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) ![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)
- Top languages - Top languages
[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats) [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats)
- WakaTime card - Wakatime card
[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) [![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats)
* * * ---
### Quick Tip (Align The Repo Cards) ### Quick Tip (Align The Repo Cards)
By default, GitHub does not lay out the cards 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:
```html ```html
<a href="https://github.com/anuraghazra/github-readme-stats"> <a href="https://github.com/anuraghazra/github-readme-stats">
@@ -408,60 +407,53 @@ By default, GitHub does not lay out the cards side by side. To do that, you can
#### [Check Out Step By Step Video Tutorial By @codeSTACKr](https://youtu.be/n6d4KHSKqGk?t=107) #### [Check Out Step By Step Video Tutorial By @codeSTACKr](https://youtu.be/n6d4KHSKqGk?t=107)
> **Warning** Since the GitHub API only allows 5k requests per hour, my `https://github-readme-stats.vercel.app/api` could possibly 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!
> If you are on the [hobby (i.e. free)](https://vercel.com/pricing) Vercel plan, please make sure you change the `maxDuration` parameter in the [vercel.json](https://github.com/anuraghazra/github-readme-stats/blob/master/vercel.json) file from `30` to `10` (see [#1416](https://github.com/anuraghazra/github-readme-stats/issues/1416#issuecomment-950275476) for more information).
Since the GitHub API only allows 5k requests per hour, my `https://github-readme-stats.vercel.app/api` could possibly hit the rate limiter. If you host it on your own Vercel server, then you do not 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
> **Note**
> Since [#58](https://github.com/anuraghazra/github-readme-stats/pull/58), we should be able to handle more than 5k requests and have fewer issues with downtime :grin:.
[![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats) [![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats)
<details> <details>
<summary><b>:hammer_and_wrench: Step-by-step guide on setting up your own Vercel instance</b></summary> <summary><b> Guide on setting up Vercel 🔨 </b></summary>
1. Go to [vercel.com](https://vercel.com/). 1. Go to [vercel.com](https://vercel.com/)
2. Click on `Log in`. 1. Click on `Log in`
![](https://files.catbox.moe/pcxk33.png) ![](https://files.catbox.moe/tct1wg.png)
3. Sign in with GitHub by pressing `Continue with GitHub`. 1. Sign in with GitHub by pressing `Continue with GitHub`
![](https://files.catbox.moe/b9oxey.png) ![](https://files.catbox.moe/btd78j.jpeg)
4. Sign in to GitHub and allow access to all repositories if prompted. 1. Sign in to GitHub and allow access to all repositories, if prompted
5. Fork this repo. 1. Fork this repo
6. After forking the repo, open the [`vercel.json`](https://github.com/anuraghazra/github-readme-stats/blob/master/vercel.json#L5) file and change the `maxDuration` field to `10`. 1. After forking the repo, open the [`vercel.json`](https://github.com/anuraghazra/github-readme-stats/blob/master/vercel.json#L5) file and change the `maxDuration` field to `10`
7. Go back to your [Vercel dashboard](https://vercel.com/dashboard). 1. Go back to your [Vercel dashboard](https://vercel.com/dashboard)
8. To import a project, click the `Add New...` button and select the `Project` option. 1. Select `Import Project`
![](https://files.catbox.moe/3n76fh.png) ![](https://files.catbox.moe/qckos0.png)
9. Click the `Continue with GitHub` button, search for the required Git Repository and import it by clicking the `Import` button. Alternatively, you can import a Third-Party Git Repository using the `Import Third-Party Git Repository ->` link at the bottom of the page. 1. Select `Import Git Repository`. Select root and keep everything as is.
![](https://files.catbox.moe/mg5p04.png) ![](https://files.catbox.moe/pqub9q.png)
10. Create a personal access token (PAT) [here](https://github.com/settings/tokens/new) and enable the `repo` permissions (this allows access to see private repo stats). 1. Create a personal access token (PAT) [here](https://github.com/settings/tokens/new) and enable the `repo` permissions (this allows access to see private repo stats)
11. Add the PAT as an environment variable named `PAT_1` (as shown). 1. Add the PAT as an environment variable named `PAT_1` (as shown).
![](https://files.catbox.moe/0yclio.png) ![](https://files.catbox.moe/0ez4g7.png)
12. Click deploy, and you're good to go. See your domains to use the API! 1. Click deploy, and you're good to go. See your domains to use the API!
</details> </details>
### Keep your fork up to date
You can keep your fork, and thus your private Vercel instance up to date with the upstream using GitHubs' [Sync Fork button](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork). You can also use the [pull](https://github.com/wei/pull) package created by [@wei](https://github.com/wei) to automate this process.
## :sparkling_heart: Support the project ## :sparkling_heart: Support the project
I open-source almost everything I can and try to reply to everyone needing help using these projects. Obviously, 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. this takes time. You can use this service for free.
However, if you are using this project and are happy with it or just want to encourage me to continue creating stuff, there are a few ways you can do it: However, if you are using this project and are happy with it or just want to encourage me to continue creating stuff, there are a few ways you can do it:-
- Giving proper credit when you use github-readme-stats on your readme, linking back to it :D - Giving proper credit when you use github-readme-stats on your readme, linking back to it :D
- Starring and sharing the project :rocket: - 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: - [![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: Thanks! :heart:
* * * ---
[![https://vercel.com?utm_source=github_readme_stats_team&utm_campaign=oss](./powered-by-vercel.svg)](https://vercel.com?utm_source=github_readme_stats_team&utm_campaign=oss) [![https://vercel.com?utm_source=github_readme_stats_team&utm_campaign=oss](./powered-by-vercel.svg)](https://vercel.com?utm_source=github_readme_stats_team&utm_campaign=oss)
Contributions are welcome! &lt;3
Contributions are welcome! <3
Made with :heart: and JavaScript. Made with :heart: and JavaScript.
+4 -24
View File
@@ -24,7 +24,7 @@ const THEME_PR_FAIL_TEXT = ":x: Theme PR does not adhere to our guidelines.";
const THEME_PR_SUCCESS_TEXT = const THEME_PR_SUCCESS_TEXT =
":heavy_check_mark: Theme PR does adhere to our guidelines."; ":heavy_check_mark: Theme PR does adhere to our guidelines.";
const FAIL_TEXT = ` const FAIL_TEXT = `
\rUnfortunately, your theme PR contains an error or does not adhere to our [theme guidelines](https://github.com/anuraghazra/github-readme-stats/blob/master/CONTRIBUTING.md#themes-contribution). Please fix the issues below, and we will review your\ \rUnfortunately, your theme PR does not adhere to our [theme guidelines](https://github.com/anuraghazra/github-readme-stats/blob/master/CONTRIBUTING.md#themes-contribution). Please fix the issues below, and we will review your\
\r PR again. This pull request will **automatically close in 15 days** if no changes are made. After this time, you must re-open the PR for it to be reviewed. \r PR again. This pull request will **automatically close in 15 days** if no changes are made. After this time, you must re-open the PR for it to be reviewed.
`; `;
const THEME_CONTRIB_GUIDELINESS = ` const THEME_CONTRIB_GUIDELINESS = `
@@ -558,30 +558,10 @@ export const run = async (prNumber) => {
info(`DRY_RUN: Review reason: ${reviewReason}`); info(`DRY_RUN: Review reason: ${reviewReason}`);
} }
} catch (error) { } catch (error) {
debug("Set review state to `REQUEST_CHANGES` and add `invalid` label...");
if (!dryRun) {
await addReview(
octokit,
pullRequestId,
owner,
repo,
"REQUEST_CHANGES",
error.message,
);
await addRemoveLabel(
octokit,
pullRequestId,
owner,
repo,
"invalid",
true,
);
} else {
info(`DRY_RUN: Review state: REQUEST_CHANGES`);
info(`DRY_RUN: Review reason: ${error.message}`);
}
setFailed(error.message); setFailed(error.message);
} }
}; };
run(); if (typeof require !== "undefined" && require.main === module) {
run();
}
+54
View File
@@ -0,0 +1,54 @@
/**
* @file Apply preview-theme script to all theme pull requests.
*/
import { debug, setFailed } from "@actions/core";
import { getGithubToken, getRepoInfo } from "./helpers.js";
import { run as previewTheme } from "./preview-theme.js";
import github from "@actions/github";
import { fetchOpenPRs, pullsWithLabel } from "./close-stale-theme-prs.js";
/**
* Get all pull request that do not contain a given label.
* @param pull The pull requests to check.
* @param label The label to check for.
* @returns Whether the pull request contains the label.
*/
export const pullsWithoutLabel = (pulls, label) => {
return pulls.filter((pr) => {
return pr.labels.nodes.some((lab) => lab.name !== label);
});
};
/**
* Main function.
*/
const run = async () => {
try {
// Create octokit client.
const dryRun = process.env.DRY_RUN === "true" || false;
debug("Creating octokit client...");
const octokit = github.getOctokit(getGithubToken());
const { owner, repo } = getRepoInfo(github.context);
// Retrieve all theme pull requests.
debug("Retrieving all theme pull requests...");
const prs = await fetchOpenPRs(octokit, owner, repo);
let themePRs = pullsWithLabel(prs, "themes");
themePRs = pullsWithoutLabel(themePRs, "invalid");
debug(`Found ${themePRs.length} theme PRs`);
// Loop through all theme PR and execute the theme-preview script.
for (const pr of themePRs) {
debug(`Applying theme-preview to #${pr.number}...`);
console.log("Pull request number: ", pr.number);
if (!dryRun) {
await previewTheme(pr.number);
}
}
} catch (error) {
setFailed(error.message);
}
};
run();
+1 -23
View File
@@ -1,14 +1,4 @@
/** // https://stackoverflow.com/a/5263759/10629172
* Calculates the probability of x taking on x or a value less than x in a normal distribution
* with mean and standard deviation.
*
* @see https://stackoverflow.com/a/5263759/10629172
*
* @param {string} mean The mean of the normal distribution.
* @param {number} sigma The standard deviation of the normal distribution.
* @param {number} to The value to calculate the probability for.
* @returns {number} Probability.
*/
function normalcdf(mean, sigma, to) { function normalcdf(mean, sigma, to) {
var z = (to - mean) / Math.sqrt(2 * sigma * sigma); var z = (to - mean) / Math.sqrt(2 * sigma * sigma);
var t = 1 / (1 + 0.3275911 * Math.abs(z)); var t = 1 / (1 + 0.3275911 * Math.abs(z));
@@ -26,18 +16,6 @@ function normalcdf(mean, sigma, to) {
return (1 / 2) * (1 + sign * erf); return (1 / 2) * (1 + sign * erf);
} }
/**
* Calculates the users rank.
*
* @param {number} totalRepos Total number of repos.
* @param {number} totalCommits Total number of commits.
* @param {number} contributions The number of contributions.
* @param {number} followers The number of followers.
* @param {number} prs The number of pull requests.
* @param {number} issues The number of issues.
* @param {number} stargazers The number of stars.
* @returns {{level: string, score: number}}} The users rank.
*/
function calculateRank({ function calculateRank({
totalRepos, totalRepos,
totalCommits, totalCommits,
-4
View File
@@ -1,4 +0,0 @@
export { renderRepoCard } from "./repo-card.js";
export { renderStatsCard } from "./stats-card.js";
export { renderTopLanguages } from "./top-languages-card.js";
export { renderWakatimeCard } from "./wakatime-card.js";
+10 -25
View File
@@ -14,11 +14,9 @@ import {
import { repoCardLocales } from "../translations.js"; import { repoCardLocales } from "../translations.js";
/** /**
* Retrieves the repository description and wraps it to fit the card width. * @param {string} label
* * @param {string} textColor
* @param {string} label The repository description. * @returns {string}
* @param {string} textColor The color of the text.
* @returns {string} Wrapped repo description SVG object.
*/ */
const getBadgeSVG = (label, textColor) => ` const getBadgeSVG = (label, textColor) => `
<g data-testid="badge" class="badge" transform="translate(320, -18)"> <g data-testid="badge" class="badge" transform="translate(320, -18)">
@@ -36,11 +34,9 @@ const getBadgeSVG = (label, textColor) => `
`; `;
/** /**
* Creates a node to display the primary programming language of the repository. * @param {string} langName
* * @param {string} langColor
* @param {string} langName Language name. * @returns {string}
* @param {string} langColor Language color.
* @returns {string} Language display SVG object.
*/ */
const createLanguageNode = (langName, langColor) => { const createLanguageNode = (langName, langColor) => {
return ` return `
@@ -52,15 +48,6 @@ const createLanguageNode = (langName, langColor) => {
}; };
const ICON_SIZE = 16; const ICON_SIZE = 16;
/**
* Creates an icon with label to display repository stats like forks, stars, etc.
*
* @param {string} icon The icon to display.
* @param {number|string} label The label to display.
* @param {string} testid The testid to assign to the label.
* @returns {string} Icon with label SVG object.
*/
const iconWithLabel = (icon, label, testid) => { const iconWithLabel = (icon, label, testid) => {
if (label <= 0) return ""; if (label <= 0) return "";
const iconSvg = ` const iconSvg = `
@@ -80,11 +67,9 @@ const iconWithLabel = (icon, label, testid) => {
}; };
/** /**
* Renders repository card details. * @param {import('../fetchers/types').RepositoryData} repo
* * @param {Partial<import("./types").RepoCardOptions>} options
* @param {import('../fetchers/types').RepositoryData} repo Repository data. * @returns {string}
* @param {Partial<import("./types").RepoCardOptions>} options Card options.
* @returns {string} Repository card SVG object.
*/ */
const renderRepoCard = (repo, options = {}) => { const renderRepoCard = (repo, options = {}) => {
const { const {
@@ -103,7 +88,7 @@ const renderRepoCard = (repo, options = {}) => {
icon_color, icon_color,
text_color, text_color,
bg_color, bg_color,
show_owner = false, show_owner,
theme = "default_repocard", theme = "default_repocard",
border_radius, border_radius,
border_color, border_color,
+3 -18
View File
@@ -12,19 +12,6 @@ import {
import { getStyles } from "../getStyles.js"; import { getStyles } from "../getStyles.js";
import { statCardLocales } from "../translations.js"; import { statCardLocales } from "../translations.js";
/**
* Create a stats card text item.
*
* @param {object[]} createTextNodeParams Object that contains the createTextNode parameters.
* @param {string} createTextNodeParams.label The label to display.
* @param {string} createTextNodeParams.value The value to display.
* @param {string} createTextNodeParams.id The id of the stat.
* @param {number} createTextNodeParams.index The index of the stat.
* @param {boolean} createTextNodeParams.showIcons Whether to show icons.
* @param {number} createTextNodeParams.shiftValuePos Number of pixels the value has to be shifted to the right.
* @param {boolean} createTextNodeParams.bold Whether to bold the label.
* @returns
*/
const createTextNode = ({ const createTextNode = ({
icon, icon,
label, label,
@@ -63,11 +50,9 @@ const createTextNode = ({
}; };
/** /**
* Renders the stats card. * @param {Partial<import('../fetchers/types').StatsData>} stats
* * @param {Partial<import("./types").StatCardOptions>} options
* @param {Partial<import('../fetchers/types').StatsData>} stats The stats data. * @returns {string}
* @param {Partial<import("./types").StatCardOptions>} options The card options.
* @returns {string} The stats card SVG object.
*/ */
const renderStatsCard = (stats = {}, options = { hide: [] }) => { const renderStatsCard = (stats = {}, options = { hide: [] }) => {
const { const {
+28 -55
View File
@@ -23,10 +23,7 @@ const CARD_PADDING = 25;
*/ */
/** /**
* Retrieves the programming language whose name is the longest. * @param {Lang[]} arr
*
* @param {Lang[]} arr Array of programming languages.
* @returns {Object} Longest programming language object.
*/ */
const getLongestLang = (arr) => const getLongestLang = (arr) =>
arr.reduce( arr.reduce(
@@ -36,15 +33,12 @@ const getLongestLang = (arr) =>
); );
/** /**
* Creates a node to display usage of a programming language in percentage * @param {{
* using text and a horizontal progress bar. * width: number,
* * color: string,
* @param {object[]} props Function properties. * name: string,
* @param {number} props.width The card width * progress: string
* @param {string} props.name Name of the programming language. * }} props
* @param {string} props.color Color of the programming language.
* @param {string} props.progress Usage of the programming language in percentage.
* @returns {string} Programming language SVG node.
*/ */
const createProgressTextNode = ({ width, color, name, progress }) => { const createProgressTextNode = ({ width, color, name, progress }) => {
const paddingRight = 95; const paddingRight = 95;
@@ -66,12 +60,7 @@ const createProgressTextNode = ({ width, color, name, progress }) => {
}; };
/** /**
* Creates a text only node to display usage of a programming language in percentage. * @param {{ lang: Lang, totalSize: number }} props
*
* @param {object[]} props Function properties.
* @param {Lang} props.lang Programming language object.
* @param {number} props.totalSize Total size of all languages.
* @returns {string} Compact layout programming language SVG node.
*/ */
const createCompactLangNode = ({ lang, totalSize }) => { const createCompactLangNode = ({ lang, totalSize }) => {
const percentage = ((lang.size / totalSize) * 100).toFixed(2); const percentage = ((lang.size / totalSize) * 100).toFixed(2);
@@ -88,12 +77,7 @@ const createCompactLangNode = ({ lang, totalSize }) => {
}; };
/** /**
* Creates compact layout of text only language nodes. * @param {{ langs: Lang[], totalSize: number }} props
*
* @param {object[]} props Function properties.
* @param {Lang[]} props.langs Array of programming languages.
* @param {number} props.totalSize Total size of all languages.
* @returns {string} Programming languages SVG node.
*/ */
const createLanguageTextNode = ({ langs, totalSize }) => { const createLanguageTextNode = ({ langs, totalSize }) => {
const longestLang = getLongestLang(langs); const longestLang = getLongestLang(langs);
@@ -125,12 +109,10 @@ const createLanguageTextNode = ({ langs, totalSize }) => {
}; };
/** /**
* Renders layout to display user's most frequently used programming languages. * @param {Lang[]} langs
* * @param {number} width
* @param {Lang[]} langs Array of programming languages. * @param {number} totalLanguageSize
* @param {number} width Card width. * @returns {string}
* @param {number} totalLanguageSize Total size of all languages.
* @returns {string} Normal layout card SVG object.
*/ */
const renderNormalLayout = (langs, width, totalLanguageSize) => { const renderNormalLayout = (langs, width, totalLanguageSize) => {
return flexLayout({ return flexLayout({
@@ -148,12 +130,10 @@ const renderNormalLayout = (langs, width, totalLanguageSize) => {
}; };
/** /**
* Renders compact layout to display user's most frequently used programming languages. * @param {Lang[]} langs
* * @param {number} width
* @param {Lang[]} langs Array of programming languages. * @param {number} totalLanguageSize
* @param {number} width Card width. * @returns {string}
* @param {number} totalLanguageSize Total size of all languages.
* @returns {string} Compact layout card SVG object.
*/ */
const renderCompactLayout = (langs, width, totalLanguageSize) => { const renderCompactLayout = (langs, width, totalLanguageSize) => {
const paddingRight = 50; const paddingRight = 50;
@@ -201,31 +181,26 @@ const renderCompactLayout = (langs, width, totalLanguageSize) => {
}; };
/** /**
* Calculates height for the compact layout. * @param {number} totalLangs
* * @returns {number}
* @param {number} totalLangs Total number of languages.
* @returns {number} Card height.
*/ */
const calculateCompactLayoutHeight = (totalLangs) => { const calculateCompactLayoutHeight = (totalLangs) => {
return 90 + Math.round(totalLangs / 2) * 25; return 90 + Math.round(totalLangs / 2) * 25;
}; };
/** /**
* Calculates height for the normal layout. * @param {number} totalLangs
* * @returns {number}
* @param {number} totalLangs Total number of languages.
* @returns {number} Card height.
*/ */
const calculateNormalLayoutHeight = (totalLangs) => { const calculateNormalLayoutHeight = (totalLangs) => {
return 45 + (totalLangs + 1) * 40; return 45 + (totalLangs + 1) * 40;
}; };
/** /**
* Hides languages and trims the list to show only the top N languages.
* *
* @param {Record<string, Lang>} topLangs Top languages. * @param {Record<string, Lang>} topLangs
* @param {string[]} hide Languages to hide. * @param {string[]} hide
* @param {string} langs_count Number of languages to show. * @param {string} langs_count
*/ */
const useLanguages = (topLangs, hide, langs_count) => { const useLanguages = (topLangs, hide, langs_count) => {
let langs = Object.values(topLangs); let langs = Object.values(topLangs);
@@ -254,15 +229,13 @@ const useLanguages = (topLangs, hide, langs_count) => {
}; };
/** /**
* Renders card to display user's most frequently used programming languages. * @param {import('../fetchers/types').TopLangData} topLangs
* * @param {Partial<import("./types").TopLangOptions>} options
* @param {import('../fetchers/types').TopLangData} topLangs User's most frequently used programming languages. * @returns {string}
* @param {Partial<import("./types").TopLangOptions>} options Card options.
* @returns {string} Language card SVG object.
*/ */
const renderTopLanguages = (topLangs, options = {}) => { const renderTopLanguages = (topLangs, options = {}) => {
const { const {
hide_title = false, hide_title,
hide_border, hide_border,
card_width, card_width,
title_color, title_color,
+29 -35
View File
@@ -23,9 +23,7 @@ const require = createRequire(import.meta.url);
const languageColors = require("../common/languageColors.json"); // now works const languageColors = require("../common/languageColors.json"); // now works
/** /**
* Creates the no coding activity SVG node. * @param {{color: string, text: string}} param0
*
* @param {{color: string, text: string}} The function prop
*/ */
const noCodingActivityNode = ({ color, text }) => { const noCodingActivityNode = ({ color, text }) => {
return ` return `
@@ -34,13 +32,13 @@ const noCodingActivityNode = ({ color, text }) => {
}; };
/** /**
* Create compact WakaTime layout.
* *
* @param {Object[]} args The function arguments. * @param {{
* @param {import("../fetchers/types").WakaTimeLang[]} languages The languages array. * lang: import("../fetchers/types").WakaTimeLang,
* @param {number} totalSize The total size of the languages. * totalSize: number,
* @param {number} x The x position of the language node. * x: number,
* @param {number} y The y position of the language node. * y: number
* }} props
*/ */
const createCompactLangNode = ({ lang, totalSize, x, y }) => { const createCompactLangNode = ({ lang, totalSize, x, y }) => {
const color = languageColors[lang.name] || "#858585"; const color = languageColors[lang.name] || "#858585";
@@ -56,13 +54,12 @@ const createCompactLangNode = ({ lang, totalSize, x, y }) => {
}; };
/** /**
* Create WakaTime language text node item. * @param {{
* * langs: import("../fetchers/types").WakaTimeLang[],
* @param {Object[]} args The function arguments. * totalSize: number,
* @param {import("../fetchers/types").WakaTimeLang} lang The language object. * x: number,
* @param {number} totalSize The total size of the languages. * y: number
* @param {number} x The x position of the language node. * }} props
* @param {number} y The y position of the language node.
*/ */
const createLanguageTextNode = ({ langs, totalSize, x, y }) => { const createLanguageTextNode = ({ langs, totalSize, x, y }) => {
return langs.map((lang, index) => { return langs.map((lang, index) => {
@@ -84,16 +81,17 @@ const createLanguageTextNode = ({ langs, totalSize, x, y }) => {
}; };
/** /**
* Create WakaTime text item.
* *
* @param {Object[]} args The function arguments. * @param {{
* @param {string} id The id of the text node item. * id: string;
* @param {string} label The label of the text node item. * label: string;
* @param {string} value The value of the text node item. * value: string;
* @param {number} index The index of the text node item. * index: number;
* @param {percent} percent Percentage of the text node item. * percent: number;
* @param {boolean} hideProgress Whether to hide the progress bar. * hideProgress: boolean;
* @param {string} progressBarBackgroundColor The color of the progress bar background. * progressBarColor: string;
* progressBarBackgroundColor: string
* }} props
*/ */
const createTextNode = ({ const createTextNode = ({
id, id,
@@ -134,13 +132,11 @@ const createTextNode = ({
}; };
/** /**
* Recalculating percentages so that, compact layout's progress bar does not break when * @param {import("../fetchers/types").WakaTimeLang[]} languages
* hiding languages.
*
* @param {import("../fetchers/types").WakaTimeLang[]} languages The languages array.
* @return {import("../fetchers/types").WakaTimeLang[]} The recalculated languages array.
*/ */
const recalculatePercentages = (languages) => { const recalculatePercentages = (languages) => {
// recalculating percentages so that,
// compact layout's progress bar does not break when hiding languages
const totalSum = languages.reduce( const totalSum = languages.reduce(
(totalSum, language) => totalSum + language.percent, (totalSum, language) => totalSum + language.percent,
0, 0,
@@ -152,11 +148,9 @@ const recalculatePercentages = (languages) => {
}; };
/** /**
* Renders WakaTime card. * @param {Partial<import('../fetchers/types').WakaTimeData>} stats
* * @param {Partial<import('./types').WakaTimeOptions>} options
* @param {Partial<import('../fetchers/types').WakaTimeData>} stats WakaTime stats. * @returns {string}
* @param {Partial<import('./types').WakaTimeOptions>} options Card options.
* @returns {string} WakaTime card SVG.
*/ */
const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
let { languages } = stats; let { languages } = stats;
+8 -10
View File
@@ -3,16 +3,14 @@ import { encodeHTML, flexLayout } from "./utils.js";
class Card { class Card {
/** /**
* Creates a new card instance. * @param {object} args
* * @param {number?=} args.width
* @param {object} args Card arguments. * @param {number?=} args.height
* @param {number?=} args.width Card width. * @param {number?=} args.border_radius
* @param {number?=} args.height Card height. * @param {string?=} args.customTitle
* @param {number?=} args.border_radius Card border radius. * @param {string?=} args.defaultTitle
* @param {string?=} args.customTitle Card custom title. * @param {string?=} args.titlePrefixIcon
* @param {string?=} args.defaultTitle Card default title. * @param {ReturnType<import('../common/utils.js').getCardColors>?=} args.colors
* @param {string?=} args.titlePrefixIcon Card title prefix icon.
* @returns {Card} Card instance.
*/ */
constructor({ constructor({
width = 100, width = 100,
-3
View File
@@ -1,6 +1,3 @@
/**
* I18n translation class.
*/
class I18n { class I18n {
constructor({ locale, translations }) { constructor({ locale, translations }) {
this.locale = locale; this.locale = locale;
-12
View File
@@ -1,17 +1,5 @@
import { clampValue } from "./utils.js"; import { clampValue } from "./utils.js";
/**
* Create a node to indicate progress in percentage along a horizontal line.
*
* @param {Object} createProgressNodeParams Object that contains the createProgressNode parameters.
* @param {number} createProgressNodeParams.x X-axis position.
* @param {number} createProgressNodeParams.y Y-axis position.
* @param {number} createProgressNodeParams.width Width of progress bar.
* @param {string} createProgressNodeParams.color Progress color.
* @param {string} createProgressNodeParams.progress Progress value.
* @param {string} createProgressNodeParams.progressBarBackgroundColor Progress bar bg color.
* @returns {string} Progress node.
*/
const createProgressNode = ({ const createProgressNode = ({
x, x,
y, y,
-30
View File
@@ -1,30 +0,0 @@
export { blacklist } from "./blacklist.js";
export { Card } from "./Card.js";
export { createProgressNode } from "./createProgressNode.js";
export { I18n } from "./I18n.js";
export { icons } from "./icons.js";
export { retryer } from "./retryer.js";
export {
ERROR_CARD_LENGTH,
renderError,
encodeHTML,
kFormatter,
isValidHexColor,
parseBoolean,
parseArray,
clampValue,
isValidGradient,
fallbackColor,
request,
flexLayout,
getCardColors,
wrapTextMultiline,
logger,
CONSTANTS,
CustomError,
MissingParamError,
measureText,
lowercaseTrim,
chunkArray,
parseEmojis,
} from "./utils.js";
-11
View File
@@ -1,14 +1,5 @@
import { CustomError, logger } from "./utils.js"; import { CustomError, logger } from "./utils.js";
/**
* Try to execute the fetcher function until it succeeds or the max number of retries is reached.
*
* @param {object[]} retryerParams Object that contains the createTextNode parameters.
* @param {object[]} retryerParams.fetcher The fetcher function.
* @param {object[]} retryerParams.variables Object with arguments to pass to the fetcher function.
* @param {number} retryerParams.retries How many times to retry.
* @returns Promise<retryer>
*/
const retryer = async (fetcher, variables, retries = 0) => { const retryer = async (fetcher, variables, retries = 0) => {
if (retries > 7) { if (retries > 7) {
throw new CustomError("Maximum retries exceeded", CustomError.MAX_RETRY); throw new CustomError("Maximum retries exceeded", CustomError.MAX_RETRY);
@@ -45,8 +36,6 @@ const retryer = async (fetcher, variables, retries = 0) => {
retries++; retries++;
// directly return from the function // directly return from the function
return retryer(fetcher, variables, retries); return retryer(fetcher, variables, retries);
} else {
return err.response;
} }
} }
}; };
+67 -115
View File
@@ -4,27 +4,20 @@ import toEmoji from "emoji-name-map";
import wrap from "word-wrap"; import wrap from "word-wrap";
import { themes } from "../../themes/index.js"; import { themes } from "../../themes/index.js";
// Script parameters.
const ERROR_CARD_LENGTH = 576.5;
/** /**
* Renders error message on the card. * @param {string} message
* * @param {string} secondaryMessage
* @param {string} message Main error message. * @returns {string}
* @param {string} secondaryMessage The secondary error message.
* @returns {string} The SVG markup.
*/ */
const renderError = (message, secondaryMessage = "") => { const renderError = (message, secondaryMessage = "") => {
return ` return `
<svg width="${ERROR_CARD_LENGTH}" height="120" viewBox="0 0 ${ERROR_CARD_LENGTH} 120" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="495" height="120" viewBox="0 0 495 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<style> <style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2F80ED } .text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2F80ED }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #252525 } .small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #252525 }
.gray { fill: #858585 } .gray { fill: #858585 }
</style> </style>
<rect x="0.5" y="0.5" width="${ <rect x="0.5" y="0.5" width="494" height="99%" rx="4.5" fill="#FFFEFE" stroke="#E4E2E2"/>
ERROR_CARD_LENGTH - 1
}" height="99%" rx="4.5" fill="#FFFEFE" stroke="#E4E2E2"/>
<text x="25" y="45" class="text">Something went wrong! file an issue at https://tiny.one/readme-stats</text> <text x="25" y="45" class="text">Something went wrong! file an issue at https://tiny.one/readme-stats</text>
<text data-testid="message" x="25" y="55" class="text small"> <text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">${encodeHTML(message)}</tspan> <tspan x="25" dy="18">${encodeHTML(message)}</tspan>
@@ -35,12 +28,9 @@ const renderError = (message, secondaryMessage = "") => {
}; };
/** /**
* Encode string as HTML.
*
* @see https://stackoverflow.com/a/48073476/10629172 * @see https://stackoverflow.com/a/48073476/10629172
* * @param {string} str
* @param {string} str String to encode. * @returns {string}
* @returns {string} Encoded string.
*/ */
function encodeHTML(str) { function encodeHTML(str) {
return str return str
@@ -51,10 +41,7 @@ function encodeHTML(str) {
} }
/** /**
* Retrieves num with suffix k(thousands) precise to 1 decimal if greater than 999. * @param {number} num
*
* @param {number} num The number to format.
* @returns {string|number} The formatted number.
*/ */
function kFormatter(num) { function kFormatter(num) {
return Math.abs(num) > 999 return Math.abs(num) > 999
@@ -63,12 +50,8 @@ function kFormatter(num) {
} }
/** /**
* Checks if a string is a valid hex color. * @param {string} hexColor
* * @returns {boolean}
* @param {string} hexColor String to check.
* @returns {boolean} True if the given string is a valid hex color.
*
* @description: Tests color codes without the hash (#) prefix.
*/ */
function isValidHexColor(hexColor) { function isValidHexColor(hexColor) {
return new RegExp( return new RegExp(
@@ -77,10 +60,8 @@ function isValidHexColor(hexColor) {
} }
/** /**
* Returns boolean if value is either "true" or "false" else the value as it is. * @param {string} value
* * @returns {boolean | string}
* @param {string} value The value to parse.
* @returns {boolean | string} The parsed value.
*/ */
function parseBoolean(value) { function parseBoolean(value) {
if (value === "true") { if (value === "true") {
@@ -93,10 +74,7 @@ function parseBoolean(value) {
} }
/** /**
* Parse string to array of strings. * @param {string} str
*
* @param {string} str The string to parse.
* @returns {string[]} The array of strings.
*/ */
function parseArray(str) { function parseArray(str) {
if (!str) return []; if (!str) return [];
@@ -104,12 +82,9 @@ function parseArray(str) {
} }
/** /**
* Clamp the given number between the given range. * @param {number} number
* * @param {number} min
* @param {number} number The number to clamp. * @param {number} max
* @param {number} min The minimum value.
* @param {number} max The maximum value.
* returns {number} The clamped number.
*/ */
function clampValue(number, min, max) { function clampValue(number, min, max) {
// @ts-ignore // @ts-ignore
@@ -118,22 +93,16 @@ function clampValue(number, min, max) {
} }
/** /**
* Check if the given string is a valid gradient. * @param {string[]} colors
*
* @param {string[]} colors Array of colors.
* @returns {boolean} True if the given string is a valid gradient.
*/ */
function isValidGradient(colors) { function isValidGradient(colors) {
if (isNaN(colors[0])) return false; return isValidHexColor(colors[1]) && isValidHexColor(colors[2]);
return colors.slice(1).every((color) => isValidHexColor(color));
} }
/** /**
* Retrieves a gradient if color has more than one valid hex codes else a single color. * @param {string} color
* * @param {string} fallbackColor
* @param {string} color The color to parse. * @returns {string | string[]}
* @param {string} fallbackColor The fallback color.
* @returns {string | string[]} The gradient or color.
*/ */
function fallbackColor(color, fallbackColor) { function fallbackColor(color, fallbackColor) {
let colors = color.split(","); let colors = color.split(",");
@@ -150,11 +119,8 @@ function fallbackColor(color, fallbackColor) {
} }
/** /**
* Send GraphQL request to GitHub API. * @param {import('axios').AxiosRequestConfig['data']} data
* * @param {import('axios').AxiosRequestConfig['headers']} headers
* @param {import('axios').AxiosRequestConfig['data']} data Request data.
* @param {import('axios').AxiosRequestConfig['headers']} headers Request headers.
* @returns {Promise<any>} Request response.
*/ */
function request(data, headers) { function request(data, headers) {
// @ts-ignore // @ts-ignore
@@ -167,15 +133,17 @@ function request(data, headers) {
} }
/** /**
* Auto layout utility, allows us to layout things vertically or horizontally with * @param {object} props
* proper gaping. * @param {string[]} props.items
* @param {number} props.gap
* @param {number[]?=} props.sizes
* @param {"column" | "row"?=} props.direction
* *
* @param {object} props Function properties. * @returns {string[]}
* @param {string[]} props.items Array of items to layout. *
* @param {number} props.gap Gap between items. * @description
* @param {"column" | "row"?=} props.direction Direction to layout items. * Auto layout utility, allows us to layout things
* @param {number[]?=} props.sizes Array of sizes for each item. * vertically or horizontally with proper gaping
* @returns {string[]} Array of items with proper layout.
*/ */
function flexLayout({ items, gap, direction, sizes = [] }) { function flexLayout({ items, gap, direction, sizes = [] }) {
let lastSize = 0; let lastSize = 0;
@@ -192,17 +160,18 @@ function flexLayout({ items, gap, direction, sizes = [] }) {
} }
/** /**
* Returns theme based colors with proper overrides and defaults. * @typedef {object} CardColors
* * @prop {string?=} title_color
* @param {Object[]} args Function arguments. * @prop {string?=} text_color
* @param {string} args.title_color Card title color. * @prop {string?=} icon_color
* @param {string} args.text_color Card text color. * @prop {string?=} bg_color
* @param {string} args.icon_color Card icon color. * @prop {string?=} border_color
* @param {string} args.bg_color Card background color. * @prop {keyof typeof import('../../themes')?=} fallbackTheme
* @param {string} args.border_color Card border color. * @prop {keyof typeof import('../../themes')?=} theme
* @param {string} args.theme Card theme. */
* @param {string} args.fallbackTheme Fallback theme. /**
* * returns theme based colors with proper overrides and defaults
* @param {CardColors} options
*/ */
function getCardColors({ function getCardColors({
title_color, title_color,
@@ -246,12 +215,10 @@ function getCardColors({
} }
/** /**
* Split text over multiple lines based on the card width. * @param {string} text
* * @param {number} width
* @param {string} text Text to split. * @param {number} maxLines
* @param {number} width Line width in number of characters. * @returns {string[]}
* @param {number} maxLines Maximum number of lines.
* @returns {string[]} Array of lines.
*/ */
function wrapTextMultiline(text, width = 59, maxLines = 3) { function wrapTextMultiline(text, width = 59, maxLines = 3) {
const fullWidthComma = ""; const fullWidthComma = "";
@@ -296,16 +263,12 @@ const SECONDARY_ERROR_MESSAGES = {
MAX_RETRY: MAX_RETRY:
"Please add an env variable called PAT_1 with your github token in vercel", "Please add an env variable called PAT_1 with your github token in vercel",
USER_NOT_FOUND: "Make sure the provided username is not an organization", USER_NOT_FOUND: "Make sure the provided username is not an organization",
GRAPHQL_ERROR: "Please try again later",
}; };
/**
* Custom error class to handle custom GRS errors.
*/
class CustomError extends Error { class CustomError extends Error {
/** /**
* @param {string} message Error message. * @param {string} message
* @param {string} type Error type. * @param {string} type
*/ */
constructor(message, type) { constructor(message, type) {
super(message); super(message);
@@ -315,12 +278,8 @@ class CustomError extends Error {
static MAX_RETRY = "MAX_RETRY"; static MAX_RETRY = "MAX_RETRY";
static USER_NOT_FOUND = "USER_NOT_FOUND"; static USER_NOT_FOUND = "USER_NOT_FOUND";
static GRAPHQL_ERROR = "GRAPHQL_ERROR";
} }
/**
* Missing query parameter class.
*/
class MissingParamError extends Error { class MissingParamError extends Error {
/** /**
* @param {string[]} missedParams * @param {string[]} missedParams
@@ -337,12 +296,10 @@ class MissingParamError extends Error {
} }
/** /**
* Retrieve text length.
*
* @see https://stackoverflow.com/a/48172630/10629172 * @see https://stackoverflow.com/a/48172630/10629172
* @param {string} str String to measure. * @param {string} str
* @param {number} fontSize Font size. * @param {number} fontSize
* @returns {number} Text length. * @returns
*/ */
function measureText(str, fontSize = 10) { function measureText(str, fontSize = 10) {
// prettier-ignore // prettier-ignore
@@ -382,12 +339,10 @@ function measureText(str, fontSize = 10) {
const lowercaseTrim = (name) => name.toLowerCase().trim(); const lowercaseTrim = (name) => name.toLowerCase().trim();
/** /**
* Split array of languages in columns. * @template T
* * @param {Array<T>} arr
* @template T Langauge object. * @param {number} perChunk
* @param {Array<T>} arr Array of languages. * @returns {Array<T>}
* @param {number} perChunk Number of languages per column.
* @returns {Array<T>} Array of languages split in columns.
*/ */
function chunkArray(arr, perChunk) { function chunkArray(arr, perChunk) {
return arr.reduce((resultArray, item, index) => { return arr.reduce((resultArray, item, index) => {
@@ -404,38 +359,35 @@ function chunkArray(arr, perChunk) {
} }
/** /**
* Parse emoji from string.
* *
* @param {string} str String to parse emoji from. * @param {string} str
* @returns {string} String with emoji parsed. * @returns {string}
*/ */
function parseEmojis(str) { function parseEmojis(str) {
if (!str) throw new Error("[parseEmoji]: str argument not provided"); if (!str) throw new Error("[parseEmoji]: str argument not provided");
return str.replace(/:[\w+-]+:/gm, (emoji) => { return str.replace(/:\w+:/gm, (emoji) => {
return toEmoji.get(emoji) || ""; return toEmoji.get(emoji) || "";
}); });
} }
export { export {
ERROR_CARD_LENGTH,
renderError, renderError,
encodeHTML,
kFormatter, kFormatter,
encodeHTML,
isValidHexColor, isValidHexColor,
parseBoolean,
parseArray,
clampValue,
isValidGradient,
fallbackColor,
request, request,
parseArray,
parseBoolean,
fallbackColor,
flexLayout, flexLayout,
getCardColors, getCardColors,
clampValue,
wrapTextMultiline, wrapTextMultiline,
measureText,
logger, logger,
CONSTANTS, CONSTANTS,
CustomError, CustomError,
MissingParamError, MissingParamError,
measureText,
lowercaseTrim, lowercaseTrim,
chunkArray, chunkArray,
parseEmojis, parseEmojis,
+5 -10
View File
@@ -3,11 +3,8 @@ import { retryer } from "../common/retryer.js";
import { MissingParamError, request } from "../common/utils.js"; import { MissingParamError, request } from "../common/utils.js";
/** /**
* Repo data fetcher. * @param {import('Axios').AxiosRequestHeaders} variables
* * @param {string} token
* @param {import('Axios').AxiosRequestHeaders} variables Fetcher variables.
* @param {string} token Github token.
* @returns {Promise<import('Axios').AxiosResponse>} The response.
*/ */
const fetcher = (variables, token) => { const fetcher = (variables, token) => {
return request( return request(
@@ -54,11 +51,9 @@ const fetcher = (variables, token) => {
const urlExample = "/api/pin?username=USERNAME&amp;repo=REPO_NAME"; const urlExample = "/api/pin?username=USERNAME&amp;repo=REPO_NAME";
/** /**
* Fetch repository data. * @param {string} username
* * @param {string} reponame
* @param {string} username Github username. * @returns {Promise<import("./types").RepositoryData>}
* @param {string} reponame Github repository name.
* @returns {Promise<import("./types").RepositoryData>} Repository data.
*/ */
async function fetchRepo(username, reponame) { async function fetchRepo(username, reponame) {
if (!username && !reponame) { if (!username && !reponame) {
+20 -110
View File
@@ -9,17 +9,13 @@ import {
logger, logger,
MissingParamError, MissingParamError,
request, request,
wrapTextMultiline,
} from "../common/utils.js"; } from "../common/utils.js";
dotenv.config(); dotenv.config();
/** /**
* Stats fetcher object. * @param {import('axios').AxiosRequestHeaders} variables
* * @param {string} token
* @param {import('axios').AxiosRequestHeaders} variables Fetcher variables.
* @param {string} token Github token.
* @returns {Promise<import('../common/types').StatsFetcherResponse>} Stats fetcher response.
*/ */
const fetcher = (variables, token) => { const fetcher = (variables, token) => {
return request( return request(
@@ -33,10 +29,10 @@ const fetcher = (variables, token) => {
totalCommitContributions totalCommitContributions
restrictedContributionsCount restrictedContributionsCount
} }
repositoriesContributedTo(contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) { repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
totalCount totalCount
} }
pullRequests { pullRequests(first: 1) {
totalCount totalCount
} }
openIssues: issues(states: OPEN) { openIssues: issues(states: OPEN) {
@@ -48,44 +44,14 @@ const fetcher = (variables, token) => {
followers { followers {
totalCount totalCount
} }
repositories(ownerAffiliations: OWNER) { repositories(first: 100, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}) {
totalCount totalCount
}
}
}
`,
variables,
},
{
Authorization: `bearer ${token}`,
},
);
};
/**
* Fetch first 100 repositories for a given username.
*
* @param {import('axios').AxiosRequestHeaders} variables Fetcher variables.
* @param {string} token Github token.
* @returns {Promise<import('../common/types').StatsFetcherResponse>} Repositories fetcher response.
*/
const repositoriesFetcher = (variables, token) => {
return request(
{
query: `
query userInfo($login: String!, $after: String) {
user(login: $login) {
repositories(first: 100, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}, after: $after) {
nodes { nodes {
name name
stargazers { stargazers {
totalCount totalCount
} }
} }
pageInfo {
hasNextPage
endCursor
}
} }
} }
} }
@@ -98,15 +64,8 @@ const repositoriesFetcher = (variables, token) => {
); );
}; };
/** // https://github.com/anuraghazra/github-readme-stats/issues/92#issuecomment-661026467
* Fetch all the commits for all the repositories of a given username. // https://github.com/anuraghazra/github-readme-stats/pull/211/
*
* @param {*} username Github username.
* @returns {Promise<number>} Total commits.
*
* @description Done like this because the Github API does not provide a way to fetch all the commits. See
* #92#issuecomment-661026467 and #211 for more information.
*/
const totalCommitsFetcher = async (username) => { const totalCommitsFetcher = async (username) => {
if (!githubUsernameRegex.test(username)) { if (!githubUsernameRegex.test(username)) {
logger.log("Invalid username"); logger.log("Invalid username");
@@ -141,52 +100,10 @@ const totalCommitsFetcher = async (username) => {
}; };
/** /**
* Fetch all the stars for all the repositories of a given username. * @param {string} username
* * @param {boolean} count_private
* @param {string} username Github username. * @param {boolean} include_all_commits
* @param {array} repoToHide Repositories to hide. * @returns {Promise<import("./types").StatsData>}
* @returns {Promise<number>} Total stars.
*/
const totalStarsFetcher = async (username, repoToHide) => {
let nodes = [];
let hasNextPage = true;
let endCursor = null;
while (hasNextPage) {
const variables = { login: username, first: 100, after: endCursor };
let res = await retryer(repositoriesFetcher, variables);
if (res.data.errors) {
logger.error(res.data.errors);
throw new CustomError(
res.data.errors[0].message || "Could not fetch user",
CustomError.USER_NOT_FOUND,
);
}
const allNodes = res.data.data.user.repositories.nodes;
const nodesWithStars = allNodes.filter(
(node) => node.stargazers.totalCount !== 0,
);
nodes.push(...nodesWithStars);
// hasNextPage =
// allNodes.length === nodesWithStars.length &&
// res.data.data.user.repositories.pageInfo.hasNextPage;
hasNextPage = false; // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
endCursor = res.data.data.user.repositories.pageInfo.endCursor;
}
return nodes
.filter((data) => !repoToHide[data.name])
.reduce((prev, curr) => prev + curr.stargazers.totalCount, 0);
};
/**
* Fetch stats for a given username.
*
* @param {string} username Github username.
* @param {boolean} count_private Include private contributions.
* @param {boolean} include_all_commits Include all commits.
* @returns {Promise<import("./types").StatsData>} Stats data.
*/ */
async function fetchStats( async function fetchStats(
username, username,
@@ -208,24 +125,11 @@ async function fetchStats(
let res = await retryer(fetcher, { login: username }); let res = await retryer(fetcher, { login: username });
// Catch GraphQL errors.
if (res.data.errors) { if (res.data.errors) {
logger.error(res.data.errors); logger.error(res.data.errors);
if (res.data.errors[0].type === "NOT_FOUND") {
throw new CustomError(
res.data.errors[0].message || "Could not fetch user.",
CustomError.USER_NOT_FOUND,
);
}
if (res.data.errors[0].message) {
throw new CustomError(
wrapTextMultiline(res.data.errors[0].message, 90, 1)[0],
res.statusText,
);
}
throw new CustomError( throw new CustomError(
"Something went while trying to retrieve the stats data using the GraphQL API.", res.data.errors[0].message || "Could not fetch user",
CustomError.GRAPHQL_ERROR, CustomError.USER_NOT_FOUND,
); );
} }
@@ -262,7 +166,13 @@ async function fetchStats(
stats.contributedTo = user.repositoriesContributedTo.totalCount; stats.contributedTo = user.repositoriesContributedTo.totalCount;
// Retrieve stars while filtering out repositories to be hidden // Retrieve stars while filtering out repositories to be hidden
stats.totalStars = await totalStarsFetcher(username, repoToHide); stats.totalStars = user.repositories.nodes
.filter((data) => {
return !repoToHide[data.name];
})
.reduce((prev, curr) => {
return prev + curr.stargazers.totalCount;
}, 0);
stats.rank = calculateRank({ stats.rank = calculateRank({
totalCommits: stats.totalCommits, totalCommits: stats.totalCommits,
+6 -38
View File
@@ -1,22 +1,13 @@
// @ts-check // @ts-check
import * as dotenv from "dotenv"; import * as dotenv from "dotenv";
import { retryer } from "../common/retryer.js"; import { retryer } from "../common/retryer.js";
import { import { logger, MissingParamError, request } from "../common/utils.js";
CustomError,
logger,
MissingParamError,
request,
wrapTextMultiline,
} from "../common/utils.js";
dotenv.config(); dotenv.config();
/** /**
* Top languages fetcher object. * @param {import('Axios').AxiosRequestHeaders} variables
* * @param {string} token
* @param {import('Axios').AxiosRequestHeaders} variables Fetcher variables.
* @param {string} token Github token.
* @returns {Promise<import('../common/types').StatsFetcherResponse>} Languages fetcher response.
*/ */
const fetcher = (variables, token) => { const fetcher = (variables, token) => {
return request( return request(
@@ -51,11 +42,9 @@ const fetcher = (variables, token) => {
}; };
/** /**
* Fetch top languages for a given username. * @param {string} username
* * @param {string[]} exclude_repo
* @param {string} username Github username. * @returns {Promise<import("./types").TopLangData>}
* @param {string[]} exclude_repo List of repositories to exclude.
* @returns {Promise<import("./types").TopLangData>} Top languages data.
*/ */
async function fetchTopLanguages(username, exclude_repo = []) { async function fetchTopLanguages(username, exclude_repo = []) {
if (!username) throw new MissingParamError(["username"]); if (!username) throw new MissingParamError(["username"]);
@@ -67,27 +56,6 @@ async function fetchTopLanguages(username, exclude_repo = []) {
throw Error(res.data.errors[0].message || "Could not fetch user"); throw Error(res.data.errors[0].message || "Could not fetch user");
} }
// Catch GraphQL errors.
if (res.data.errors) {
logger.error(res.data.errors);
if (res.data.errors[0].type === "NOT_FOUND") {
throw new CustomError(
res.data.errors[0].message || "Could not fetch user.",
CustomError.USER_NOT_FOUND,
);
}
if (res.data.errors[0].message) {
throw new CustomError(
wrapTextMultiline(res.data.errors[0].message, 90, 1)[0],
res.statusText,
);
}
throw new CustomError(
"Something went while trying to retrieve the language data using the GraphQL API.",
CustomError.GRAPHQL_ERROR,
);
}
let repoNodes = res.data.data.user.repositories.nodes; let repoNodes = res.data.data.user.repositories.nodes;
let repoToHide = {}; let repoToHide = {};
+2 -4
View File
@@ -2,10 +2,8 @@ import axios from "axios";
import { MissingParamError } from "../common/utils.js"; import { MissingParamError } from "../common/utils.js";
/** /**
* WakaTime data fetcher. * @param {{username: string, api_domain: string, range: string}} props
* * @returns {Promise<WakaTimeData>}
* @param {{username: string, api_domain: string, range: string}} props Fetcher props.
* @returns {Promise<WakaTimeData>} WakaTime data response.
*/ */
const fetchWakatimeStats = async ({ username, api_domain, range }) => { const fetchWakatimeStats = async ({ username, api_domain, range }) => {
if (!username) throw new MissingParamError(["username"]); if (!username) throw new MissingParamError(["username"]);
+10 -22
View File
@@ -1,9 +1,6 @@
// @ts-check // @ts-check
/** /**
* Calculates progress along the boundary of the circle i.e it's circumference. * @param {number} value
*
* @param {number} value The rank value to calculate progress for.
* @returns {number} Progress value.
*/ */
const calculateCircleProgress = (value) => { const calculateCircleProgress = (value) => {
const radius = 40; const radius = 40;
@@ -16,11 +13,9 @@ const calculateCircleProgress = (value) => {
}; };
/** /**
* Retrieves the animation to display progress along the circumference of circle
* from the beginning to the given value in a clockwise direction.
* *
* @param {{progress: number}} progress The progress value to animate to. * @param {{progress: number}} param0
* @returns {string} Progress animation css. * @returns
*/ */
const getProgressAnimation = ({ progress }) => { const getProgressAnimation = ({ progress }) => {
return ` return `
@@ -35,11 +30,6 @@ const getProgressAnimation = ({ progress }) => {
`; `;
}; };
/**
* Retrieves css animations for a card.
*
* @returns {string} Animation css.
*/
const getAnimations = () => { const getAnimations = () => {
return ` return `
/* Animations */ /* Animations */
@@ -63,15 +53,13 @@ const getAnimations = () => {
}; };
/** /**
* Retrieves CSS styles for a card. * @param {{
* * titleColor?: string | string[]
* @param {Object[]} colors The colors to use for the card. * textColor?: string | string[]
* @param {string} colors.titleColor The title color. * iconColor?: string | string[]
* @param {string} colors.textColor The text color. * show_icons?: boolean;
* @param {string} colors.iconColor The icon color. * progress?: number;
* @param {boolean} colors.show_icons Whether to show icons. * }} args
* @param {number} colors.progress The progress value to animate to.
* @returns {string} Card CSS styles.
*/ */
const getStyles = ({ const getStyles = ({
titleColor, titleColor,
-3
View File
@@ -1,3 +0,0 @@
export * from "./common/index.js";
export * from "./cards/index.js";
export { getStyles, getAnimations } from "./getStyles.js";
-13
View File
@@ -1,12 +1,5 @@
import { encodeHTML } from "./common/utils.js"; import { encodeHTML } from "./common/utils.js";
/**
* Retrieves stat card labels in the available locales.
*
* @param {string} name The name of the locale.
* @param {string} apostrophe Whether to use apostrophe or not.
* @returns {Object} The locales object.
*/
const statCardLocales = ({ name, apostrophe }) => { const statCardLocales = ({ name, apostrophe }) => {
const encodedName = encodeHTML(name); const encodedName = encodeHTML(name);
return { return {
@@ -361,12 +354,6 @@ const wakatimeCardLocales = {
const availableLocales = Object.keys(repoCardLocales["repocard.archived"]); const availableLocales = Object.keys(repoCardLocales["repocard.archived"]);
/**
* Checks whether the locale is available or not.
*
* @param {string} locale The locale to check.
* @returns {boolean} Boolean specifying whether the locale is available or not.
*/
function isLocaleAvailable(locale) { function isLocaleAvailable(locale) {
return availableLocales.includes(locale.toLowerCase()); return availableLocales.includes(locale.toLowerCase());
} }
+2 -28
View File
@@ -40,20 +40,7 @@ const data = {
followers: { totalCount: 0 }, followers: { totalCount: 0 },
repositories: { repositories: {
totalCount: 1, totalCount: 1,
},
},
},
};
const repositoriesData = {
data: {
user: {
repositories: {
nodes: [{ stargazers: { totalCount: 100 } }], nodes: [{ stargazers: { totalCount: 100 } }],
pageInfo: {
hasNextPage: false,
cursor: "cursor",
},
}, },
}, },
}, },
@@ -83,11 +70,7 @@ const faker = (query, data) => {
setHeader: jest.fn(), setHeader: jest.fn(),
send: jest.fn(), send: jest.fn(),
}; };
mock mock.onPost("https://api.github.com/graphql").reply(200, data);
.onPost("https://api.github.com/graphql")
.replyOnce(200, data)
.onPost("https://api.github.com/graphql")
.replyOnce(200, repositoriesData);
return { req, res }; return { req, res };
}; };
@@ -155,6 +138,7 @@ describe("Test /api/", () => {
it("should have proper cache", async () => { it("should have proper cache", async () => {
const { req, res } = faker({}, data); const { req, res } = faker({}, data);
mock.onPost("https://api.github.com/graphql").reply(200, data);
await api(req, res); await api(req, res);
@@ -174,16 +158,6 @@ describe("Test /api/", () => {
]); ]);
}); });
it("should not store cache when error", async () => {
const { req, res } = faker({}, error);
await api(req, res);
expect(res.setHeader.mock.calls).toEqual([
["Content-Type", "image/svg+xml"],
["Cache-Control", `no-store`],
]);
});
it("should set proper cache with clamped values", async () => { it("should set proper cache with clamped values", async () => {
{ {
let { req, res } = faker({ cache_seconds: 200000 }, data); let { req, res } = faker({ cache_seconds: 200000 }, data);
-1
View File
@@ -145,7 +145,6 @@ describe("Card", () => {
"#fff", "#fff",
); );
}); });
it("should render gradient backgrounds", () => { it("should render gradient backgrounds", () => {
const { titleColor, textColor, iconColor, bgColor } = getCardColors({ const { titleColor, textColor, iconColor, bgColor } = getCardColors({
title_color: "f00", title_color: "f00",
+8 -19
View File
@@ -11,6 +11,7 @@ import { renderStatsCard } from "../../src/cards/stats-card.js";
import { renderTopLanguages } from "../../src/cards/top-languages-card.js"; import { renderTopLanguages } from "../../src/cards/top-languages-card.js";
import { renderWakatimeCard } from "../../src/cards/wakatime-card.js"; import { renderWakatimeCard } from "../../src/cards/wakatime-card.js";
// Script variables
const REPO = "dummy-cra"; const REPO = "dummy-cra";
const USER = "grsdummy"; const USER = "grsdummy";
const STATS_DATA = { const STATS_DATA = {
@@ -25,7 +26,6 @@ const STATS_DATA = {
score: 51.01013099671447, score: 51.01013099671447,
}, },
}; };
const LANGS_DATA = { const LANGS_DATA = {
TypeScript: { TypeScript: {
color: "#3178c6", color: "#3178c6",
@@ -48,7 +48,6 @@ const LANGS_DATA = {
size: 671, size: 671,
}, },
}; };
const WAKATIME_DATA = { const WAKATIME_DATA = {
human_readable_range: "last week", human_readable_range: "last week",
is_already_updating: false, is_already_updating: false,
@@ -65,7 +64,6 @@ const WAKATIME_DATA = {
username: "grsdummy", username: "grsdummy",
writes_only: false, writes_only: false,
}; };
const REPOSITORY_DATA = { const REPOSITORY_DATA = {
name: "dummy-cra", name: "dummy-cra",
nameWithOwner: "grsdummy/dummy-cra", nameWithOwner: "grsdummy/dummy-cra",
@@ -85,8 +83,6 @@ const REPOSITORY_DATA = {
starCount: 1, starCount: 1,
}; };
const CACHE_BURST_STRING = `v=${new Date().getTime()}`;
describe("Fetch Cards", () => { describe("Fetch Cards", () => {
let VERCEL_PREVIEW_URL; let VERCEL_PREVIEW_URL;
@@ -108,24 +104,19 @@ describe("Fetch Cards", () => {
// Get the Vercel preview stats card response. // Get the Vercel preview stats card response.
const serverStatsSvg = await axios.get( const serverStatsSvg = await axios.get(
`${VERCEL_PREVIEW_URL}/api?username=${USER}&${CACHE_BURST_STRING}`, `${VERCEL_PREVIEW_URL}/api?username=${USER}`,
); );
// Check if stats card from deployment matches the stats card from local. // Check if stats card from deployment matches the stats card from local.
expect(serverStatsSvg.data).toEqual(localStatsCardSVG); expect(serverStatsSvg.data).toEqual(localStatsCardSVG);
}, 7000); });
test("retrieve language card", async () => { test("retrieve language card", async () => {
expect(VERCEL_PREVIEW_URL).toBeDefined(); expect(VERCEL_PREVIEW_URL).toBeDefined();
// Check if the Vercel preview instance language card function is up and running. // Check if the Vercel preview instance language card function is up and running.
console.log(
`${VERCEL_PREVIEW_URL}/api/top-langs/?username=${USER}&${CACHE_BURST_STRING}`,
);
await expect( await expect(
axios.get( axios.get(`${VERCEL_PREVIEW_URL}/api/top-langs/?username=${USER}`),
`${VERCEL_PREVIEW_URL}/api/top-langs/?username=${USER}&${CACHE_BURST_STRING}`,
),
).resolves.not.toThrow(); ).resolves.not.toThrow();
// Get local language card. // Get local language card.
@@ -133,7 +124,7 @@ describe("Fetch Cards", () => {
// Get the Vercel preview language card response. // Get the Vercel preview language card response.
const severLanguageSVG = await axios.get( const severLanguageSVG = await axios.get(
`${VERCEL_PREVIEW_URL}/api/top-langs/?username=${USER}&${CACHE_BURST_STRING}`, `${VERCEL_PREVIEW_URL}/api/top-langs/?username=${USER}`,
); );
// Check if language card from deployment matches the local language card. // Check if language card from deployment matches the local language card.
@@ -153,7 +144,7 @@ describe("Fetch Cards", () => {
// Get the Vercel preview WakaTime card response. // Get the Vercel preview WakaTime card response.
const serverWakaTimeSvg = await axios.get( const serverWakaTimeSvg = await axios.get(
`${VERCEL_PREVIEW_URL}/api/wakatime?username=${USER}&${CACHE_BURST_STRING}`, `${VERCEL_PREVIEW_URL}/api/wakatime?username=${USER}`,
); );
// Check if WakaTime card from deployment matches the local WakaTime card. // Check if WakaTime card from deployment matches the local WakaTime card.
@@ -165,9 +156,7 @@ describe("Fetch Cards", () => {
// Check if the Vercel preview instance Repo function is up and running. // Check if the Vercel preview instance Repo function is up and running.
await expect( await expect(
axios.get( axios.get(`${VERCEL_PREVIEW_URL}/api/pin/?username=${USER}&repo=${REPO}`),
`${VERCEL_PREVIEW_URL}/api/pin/?username=${USER}&repo=${REPO}&${CACHE_BURST_STRING}`,
),
).resolves.not.toThrow(); ).resolves.not.toThrow();
// Get local repo card. // Get local repo card.
@@ -175,7 +164,7 @@ describe("Fetch Cards", () => {
// Get the Vercel preview repo card response. // Get the Vercel preview repo card response.
const serverRepoSvg = await axios.get( const serverRepoSvg = await axios.get(
`${VERCEL_PREVIEW_URL}/api/pin/?username=${USER}&repo=${REPO}&${CACHE_BURST_STRING}`, `${VERCEL_PREVIEW_URL}/api/pin/?username=${USER}&repo=${REPO}`,
); );
// Check if Repo card from deployment matches the local Repo card. // Check if Repo card from deployment matches the local Repo card.
-1
View File
@@ -23,7 +23,6 @@ const data_user = {
organization: null, organization: null,
}, },
}; };
const data_org = { const data_org = {
data: { data: {
user: null, user: null,
+73 -164
View File
@@ -19,61 +19,13 @@ const data = {
followers: { totalCount: 100 }, followers: { totalCount: 100 },
repositories: { repositories: {
totalCount: 5, totalCount: 5,
},
},
},
};
const firstRepositoriesData = {
data: {
user: {
repositories: {
nodes: [ nodes: [
{ name: "test-repo-1", stargazers: { totalCount: 100 } }, { name: "test-repo-1", stargazers: { totalCount: 100 } },
{ name: "test-repo-2", stargazers: { totalCount: 100 } }, { name: "test-repo-2", stargazers: { totalCount: 100 } },
{ name: "test-repo-3", stargazers: { totalCount: 100 } }, { name: "test-repo-3", stargazers: { totalCount: 100 } },
],
pageInfo: {
hasNextPage: true,
cursor: "cursor",
},
},
},
},
};
const secondRepositoriesData = {
data: {
user: {
repositories: {
nodes: [
{ name: "test-repo-4", stargazers: { totalCount: 50 } }, { name: "test-repo-4", stargazers: { totalCount: 50 } },
{ name: "test-repo-5", stargazers: { totalCount: 50 } }, { name: "test-repo-5", stargazers: { totalCount: 50 } },
], ],
pageInfo: {
hasNextPage: false,
cursor: "cursor",
},
},
},
},
};
const repositoriesWithZeroStarsData = {
data: {
user: {
repositories: {
nodes: [
{ name: "test-repo-1", stargazers: { totalCount: 100 } },
{ name: "test-repo-2", stargazers: { totalCount: 100 } },
{ name: "test-repo-3", stargazers: { totalCount: 100 } },
{ name: "test-repo-4", stargazers: { totalCount: 0 } },
{ name: "test-repo-5", stargazers: { totalCount: 0 } },
],
pageInfo: {
hasNextPage: true,
cursor: "cursor",
},
}, },
}, },
}, },
@@ -92,30 +44,21 @@ const error = {
const mock = new MockAdapter(axios); const mock = new MockAdapter(axios);
beforeEach(() => {
mock
.onPost("https://api.github.com/graphql")
.replyOnce(200, data)
.onPost("https://api.github.com/graphql")
.replyOnce(200, firstRepositoriesData);
// .onPost("https://api.github.com/graphql") // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
// .replyOnce(200, secondRepositoriesData); // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
});
afterEach(() => { afterEach(() => {
mock.reset(); mock.reset();
}); });
describe("Test fetchStats", () => { describe("Test fetchStats", () => {
it("should fetch correct stats", async () => { it("should fetch correct stats", async () => {
mock.onPost("https://api.github.com/graphql").reply(200, data);
let stats = await fetchStats("anuraghazra"); let stats = await fetchStats("anuraghazra");
const rank = calculateRank({ const rank = calculateRank({
totalCommits: 100, totalCommits: 100,
totalRepos: 5, totalRepos: 5,
followers: 100, followers: 100,
contributions: 61, contributions: 61,
// stargazers: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130. stargazers: 400,
stargazers: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
prs: 300, prs: 300,
issues: 200, issues: 200,
}); });
@@ -126,23 +69,81 @@ describe("Test fetchStats", () => {
totalCommits: 100, totalCommits: 100,
totalIssues: 200, totalIssues: 200,
totalPRs: 300, totalPRs: 300,
// totalStars: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130. totalStars: 400,
totalStars: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
rank, rank,
}); });
}); });
it("should stop fetching when there are repos with zero stars", async () => { it("should throw error", async () => {
mock.reset(); mock.onPost("https://api.github.com/graphql").reply(200, error);
mock
.onPost("https://api.github.com/graphql")
.replyOnce(200, data)
.onPost("https://api.github.com/graphql")
.replyOnce(200, repositoriesWithZeroStarsData);
let stats = await fetchStats("anuraghazra"); await expect(fetchStats("anuraghazra")).rejects.toThrow(
"Could not resolve to a User with the login of 'noname'.",
);
});
it("should fetch and add private contributions", async () => {
mock.onPost("https://api.github.com/graphql").reply(200, data);
let stats = await fetchStats("anuraghazra", true);
const rank = calculateRank({ const rank = calculateRank({
totalCommits: 100, totalCommits: 150,
totalRepos: 5,
followers: 100,
contributions: 61,
stargazers: 400,
prs: 300,
issues: 200,
});
expect(stats).toStrictEqual({
contributedTo: 61,
name: "Anurag Hazra",
totalCommits: 150,
totalIssues: 200,
totalPRs: 300,
totalStars: 400,
rank,
});
});
it("should fetch total commits", async () => {
mock.onPost("https://api.github.com/graphql").reply(200, data);
mock
.onGet("https://api.github.com/search/commits?q=author:anuraghazra")
.reply(200, { total_count: 1000 });
let stats = await fetchStats("anuraghazra", true, true);
const rank = calculateRank({
totalCommits: 1050,
totalRepos: 5,
followers: 100,
contributions: 61,
stargazers: 400,
prs: 300,
issues: 200,
});
expect(stats).toStrictEqual({
contributedTo: 61,
name: "Anurag Hazra",
totalCommits: 1050,
totalIssues: 200,
totalPRs: 300,
totalStars: 400,
rank,
});
});
it("should exclude stars of the `test-repo-1` repository", async () => {
mock.onPost("https://api.github.com/graphql").reply(200, data);
mock
.onGet("https://api.github.com/search/commits?q=author:anuraghazra")
.reply(200, { total_count: 1000 });
let stats = await fetchStats("anuraghazra", true, true, ["test-repo-1"]);
const rank = calculateRank({
totalCommits: 1050,
totalRepos: 5, totalRepos: 5,
followers: 100, followers: 100,
contributions: 61, contributions: 61,
@@ -154,103 +155,11 @@ describe("Test fetchStats", () => {
expect(stats).toStrictEqual({ expect(stats).toStrictEqual({
contributedTo: 61, contributedTo: 61,
name: "Anurag Hazra", name: "Anurag Hazra",
totalCommits: 100, totalCommits: 1050,
totalIssues: 200, totalIssues: 200,
totalPRs: 300, totalPRs: 300,
totalStars: 300, totalStars: 300,
rank, rank,
}); });
}); });
it("should throw error", async () => {
mock.reset();
mock.onPost("https://api.github.com/graphql").reply(200, error);
await expect(fetchStats("anuraghazra")).rejects.toThrow(
"Could not resolve to a User with the login of 'noname'.",
);
});
it("should fetch and add private contributions", async () => {
let stats = await fetchStats("anuraghazra", true);
const rank = calculateRank({
totalCommits: 150,
totalRepos: 5,
followers: 100,
contributions: 61,
// stargazers: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
stargazers: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
prs: 300,
issues: 200,
});
expect(stats).toStrictEqual({
contributedTo: 61,
name: "Anurag Hazra",
totalCommits: 150,
totalIssues: 200,
totalPRs: 300,
// totalStars: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
totalStars: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
rank,
});
});
it("should fetch total commits", async () => {
mock
.onGet("https://api.github.com/search/commits?q=author:anuraghazra")
.reply(200, { total_count: 1000 });
let stats = await fetchStats("anuraghazra", true, true);
const rank = calculateRank({
totalCommits: 1050,
totalRepos: 5,
followers: 100,
contributions: 61,
// stargazers: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
stargazers: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
prs: 300,
issues: 200,
});
expect(stats).toStrictEqual({
contributedTo: 61,
name: "Anurag Hazra",
totalCommits: 1050,
totalIssues: 200,
totalPRs: 300,
// totalStars: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
totalStars: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
rank,
});
});
it("should exclude stars of the `test-repo-1` repository", async () => {
mock
.onGet("https://api.github.com/search/commits?q=author:anuraghazra")
.reply(200, { total_count: 1000 });
let stats = await fetchStats("anuraghazra", true, true, ["test-repo-1"]);
const rank = calculateRank({
totalCommits: 1050,
totalRepos: 5,
followers: 100,
contributions: 61,
// stargazers: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
stargazers: 200, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
prs: 300,
issues: 200,
});
expect(stats).toStrictEqual({
contributedTo: 61,
name: "Anurag Hazra",
totalCommits: 1050,
totalIssues: 200,
totalPRs: 300,
// totalStars: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
totalStars: 200, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
rank,
});
});
}); });
+10 -10
View File
@@ -10,17 +10,17 @@ import "@testing-library/jest-dom";
import { themes } from "../themes/index.js"; import { themes } from "../themes/index.js";
const stats = {
name: "Anurag Hazra",
totalStars: 100,
totalCommits: 200,
totalIssues: 300,
totalPRs: 400,
contributedTo: 500,
rank: { level: "A+", score: 40 },
};
describe("Test renderStatsCard", () => { describe("Test renderStatsCard", () => {
const stats = {
name: "Anurag Hazra",
totalStars: 100,
totalCommits: 200,
totalIssues: 300,
totalPRs: 400,
contributedTo: 500,
rank: { level: "A+", score: 40 },
};
it("should render correctly", () => { it("should render correctly", () => {
document.body.innerHTML = renderStatsCard(stats); document.body.innerHTML = renderStatsCard(stats);
+18 -18
View File
@@ -9,25 +9,25 @@ import "@testing-library/jest-dom";
import { themes } from "../themes/index.js"; import { themes } from "../themes/index.js";
const langs = {
HTML: {
color: "#0f0",
name: "HTML",
size: 200,
},
javascript: {
color: "#0ff",
name: "javascript",
size: 200,
},
css: {
color: "#ff0",
name: "css",
size: 100,
},
};
describe("Test renderTopLanguages", () => { describe("Test renderTopLanguages", () => {
const langs = {
HTML: {
color: "#0f0",
name: "HTML",
size: 200,
},
javascript: {
color: "#0ff",
name: "javascript",
size: 200,
},
css: {
color: "#ff0",
name: "css",
size: 100,
},
};
it("should render correctly", () => { it("should render correctly", () => {
document.body.innerHTML = renderTopLanguages(langs); document.body.innerHTML = renderTopLanguages(langs);
+22 -152
View File
@@ -1,25 +1,30 @@
import { queryByTestId } from "@testing-library/dom"; import { queryByTestId } from "@testing-library/dom";
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { import {
chunkArray,
clampValue,
encodeHTML, encodeHTML,
fallbackColor,
flexLayout,
getCardColors, getCardColors,
isValidGradient,
isValidHexColor,
kFormatter, kFormatter,
lowercaseTrim,
measureText,
parseArray,
parseBoolean,
parseEmojis,
renderError, renderError,
wrapTextMultiline, wrapTextMultiline,
} from "../src/common/utils.js"; } from "../src/common/utils.js";
describe("Test utils.js", () => { describe("Test utils.js", () => {
it("should test kFormatter", () => {
expect(kFormatter(1)).toBe(1);
expect(kFormatter(-1)).toBe(-1);
expect(kFormatter(500)).toBe(500);
expect(kFormatter(1000)).toBe("1k");
expect(kFormatter(10000)).toBe("10k");
expect(kFormatter(12345)).toBe("12.3k");
expect(kFormatter(9900000)).toBe("9900k");
});
it("should test encodeHTML", () => {
expect(encodeHTML(`<html>hello world<,.#4^&^@%!))`)).toBe(
"&#60;html&#62;hello world&#60;,.#4^&#38;^@%!))",
);
});
it("should test renderError", () => { it("should test renderError", () => {
document.body.innerHTML = renderError("Something went wrong"); document.body.innerHTML = renderError("Something went wrong");
expect( expect(
@@ -39,111 +44,6 @@ describe("Test utils.js", () => {
).toHaveTextContent(/Secondary Message/gim); ).toHaveTextContent(/Secondary Message/gim);
}); });
it("should test encodeHTML", () => {
expect(encodeHTML(`<html>hello world<,.#4^&^@%!))`)).toBe(
"&#60;html&#62;hello world&#60;,.#4^&#38;^@%!))",
);
});
it("should test kFormatter", () => {
expect(kFormatter(1)).toBe(1);
expect(kFormatter(-1)).toBe(-1);
expect(kFormatter(500)).toBe(500);
expect(kFormatter(1000)).toBe("1k");
expect(kFormatter(10000)).toBe("10k");
expect(kFormatter(12345)).toBe("12.3k");
expect(kFormatter(9900000)).toBe("9900k");
});
it("should test isValidHexColor", () => {
expect(isValidHexColor("00f000")).toBe(true);
expect(isValidHexColor("00f0003f")).toBe(true);
expect(isValidHexColor("0f03")).toBe(true);
expect(isValidHexColor("0f0")).toBe(true);
expect(isValidHexColor("00f00X")).toBe(false);
expect(isValidHexColor("#")).toBe(false);
expect(isValidHexColor("")).toBe(false);
expect(isValidHexColor("0")).toBe(false);
expect(isValidHexColor("00")).toBe(false);
expect(isValidHexColor("00f00")).toBe(false);
expect(isValidHexColor("00f00")).toBe(false);
});
it("should test parseBoolean", () => {
expect(parseBoolean(true)).toBe(true);
expect(parseBoolean(false)).toBe(false);
expect(parseBoolean("true")).toBe(true);
expect(parseBoolean("false")).toBe(false);
expect(parseBoolean(undefined)).toBe(undefined);
});
it("should test parseArray", () => {
expect(parseArray("1")).toEqual(["1"]);
expect(parseArray("1,2,3")).toEqual(["1", "2", "3"]);
expect(parseArray("test")).toEqual(["test"]);
});
it("should test clampValue", () => {
expect(clampValue(1, 0, 2)).toBe(1);
expect(clampValue(1, 2, 3)).toBe(2);
expect(clampValue(1, 0, 0)).toBe(0);
expect(clampValue(1, 0, -1)).toBe(0);
});
it("should test isValidGradient", () => {
expect(isValidGradient(["90", "fff", "000"])).toBe(true);
expect(isValidGradient(["90", "fff", "000", "fff"])).toBe(true);
expect(isValidGradient(["fff", "fff", "fxf"])).toBe(false);
expect(isValidGradient(["90", "fff", "fxf"])).toBe(false);
expect(isValidGradient(["90", "fxf", ""])).toBe(false);
});
it("should test fallbackColor", () => {
expect(fallbackColor("fff", "000")).toBe("#fff");
expect(fallbackColor("fxf", "#000")).toBe("#000");
expect(fallbackColor("90,fff,000", "#000")).toStrictEqual([
"90",
"fff",
"000",
]);
expect(fallbackColor("90,fff,fxf", "#000")).toBe("#000");
});
it("should test flexLayout", () => {
const items = Array(2).fill(`<g></g>`);
const columnLayout = flexLayout({
items,
gap: 25,
direction: "column",
sizes: [],
});
expect(columnLayout[0]).toContain("translate(0, 0)");
expect(columnLayout[1]).toContain("translate(0, 25)");
const rowLayout = flexLayout({
items,
gap: 25,
direction: "row",
sizes: [],
});
expect(rowLayout[0]).toContain("translate(0, 0)");
expect(rowLayout[1]).toContain("translate(25, 0)");
const sizedLayout = flexLayout({
items,
gap: 25,
direction: "row",
sizes: [25, 0],
});
expect(sizedLayout[0]).toContain("translate(0, 0)");
expect(sizedLayout[1]).toContain("translate(50, 0)");
});
it("getCardColors: should return expected values", () => { it("getCardColors: should return expected values", () => {
let colors = getCardColors({ let colors = getCardColors({
title_color: "f00", title_color: "f00",
@@ -192,15 +92,16 @@ describe("Test utils.js", () => {
borderColor: "#e4e2e2", borderColor: "#e4e2e2",
}); });
}); });
});
it("wrapTextMultiline: should not wrap small texts", () => { describe("wrapTextMultiline", () => {
it("should not wrap small texts", () => {
{ {
let multiLineText = wrapTextMultiline("Small text should not wrap"); let multiLineText = wrapTextMultiline("Small text should not wrap");
expect(multiLineText).toEqual(["Small text should not wrap"]); expect(multiLineText).toEqual(["Small text should not wrap"]);
} }
}); });
it("should wrap large texts", () => {
it("wrapTextMultiline: should wrap large texts", () => {
let multiLineText = wrapTextMultiline( let multiLineText = wrapTextMultiline(
"Hello world long long long text", "Hello world long long long text",
20, 20,
@@ -208,8 +109,7 @@ describe("Test utils.js", () => {
); );
expect(multiLineText).toEqual(["Hello world long", "long long text"]); expect(multiLineText).toEqual(["Hello world long", "long long text"]);
}); });
it("should wrap large texts and limit max lines", () => {
it("wrapTextMultiline: should wrap large texts and limit max lines", () => {
let multiLineText = wrapTextMultiline( let multiLineText = wrapTextMultiline(
"Hello world long long long text", "Hello world long long long text",
10, 10,
@@ -217,41 +117,11 @@ describe("Test utils.js", () => {
); );
expect(multiLineText).toEqual(["Hello", "world long..."]); expect(multiLineText).toEqual(["Hello", "world long..."]);
}); });
it("should wrap chinese by punctuation", () => {
it("wrapTextMultiline: should wrap chinese by punctuation", () => {
let multiLineText = wrapTextMultiline( let multiLineText = wrapTextMultiline(
"专门为刚开始刷题的同学准备的算法基地,没有最细只有更细,立志用动画将晦涩难懂的算法说的通俗易懂!", "专门为刚开始刷题的同学准备的算法基地,没有最细只有更细,立志用动画将晦涩难懂的算法说的通俗易懂!",
); );
expect(multiLineText.length).toEqual(3); expect(multiLineText.length).toEqual(3);
expect(multiLineText[0].length).toEqual(18 * 8); // &#xxxxx; x 8 expect(multiLineText[0].length).toEqual(18 * 8); // &#xxxxx; x 8
}); });
it("should test measureText", () => {
expect(measureText("test")).toEqual(16.078125);
expect(measureText("test", 12)).toEqual(19.293750000000003);
});
it("should test lowercaseTrim", () => {
expect(lowercaseTrim("test")).toEqual("test");
expect(lowercaseTrim(" test ")).toEqual("test");
expect(lowercaseTrim(" Test ")).toEqual("test");
});
it("should test chunkArray", () => {
expect(chunkArray([1, 2, 3], 1)).toEqual([[1], [2], [3]]);
expect(chunkArray([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]);
expect(chunkArray([1, 2, 3, 4, 5, 6, 7], 3)).toEqual([
[1, 2, 3],
[4, 5, 6],
[7],
]);
});
it("should test parseEmojis", () => {
expect(parseEmojis("test")).toEqual("test");
expect(parseEmojis("test :smile:")).toEqual("test 😄");
expect(parseEmojis("test :smile: :+1:")).toEqual("test 😄 👍");
expect(parseEmojis("test :smile: :+1: :smile:")).toEqual("test 😄 👍 😄");
});
}); });