forked from mirrored/github-readme-stats
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
147a5d9e2a | ||
|
|
2c970b29c1 | ||
|
|
5c25f9c429 |
@@ -1,10 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
file = open('./vercel.json', 'r')
|
|
||||||
str = file.read()
|
|
||||||
file = open('./vercel.json', 'w')
|
|
||||||
|
|
||||||
str = str.replace('"maxDuration": 10', '"maxDuration": 30')
|
|
||||||
|
|
||||||
file.write(str)
|
|
||||||
file.close()
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
name: Deployment Prep
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
config:
|
|
||||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Deployment Prep
|
|
||||||
run: python ./.github/workflows/deploy-prep.py
|
|
||||||
- uses: stefanzweifel/git-auto-commit-action@v4
|
|
||||||
with:
|
|
||||||
branch: vercel
|
|
||||||
create_branch: true
|
|
||||||
push_options: "--force"
|
|
||||||
@@ -5,7 +5,6 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
e2eTests:
|
e2eTests:
|
||||||
if:
|
if:
|
||||||
github.repository == 'anuraghazra/github-readme-stats' &&
|
|
||||||
github.event_name == 'deployment_status' &&
|
github.event_name == 'deployment_status' &&
|
||||||
github.event.deployment_status.state == 'success'
|
github.event.deployment_status.state == 'success'
|
||||||
name: Perform 2e2 tests
|
name: Perform 2e2 tests
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
closeEmptyIssuesAndTemplates:
|
closeEmptyIssuesAndTemplates:
|
||||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
|
||||||
name: Close empty issues
|
name: Close empty issues
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
triage:
|
triage:
|
||||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v4
|
- uses: actions/labeler@v4
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
closeOldThemePrs:
|
closeOldThemePrs:
|
||||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
|
||||||
name: Close stale 'invalid' theme PRs
|
name: Close stale 'invalid' theme PRs
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
showAndLabelTopIssues:
|
showAndLabelTopIssues:
|
||||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
|
||||||
name: Update top issues Dashboard.
|
name: Update top issues Dashboard.
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
name: Update supported languages
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 */30 * *"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
updateLanguages:
|
|
||||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
|
||||||
name: Update supported languages
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [16.x]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
cache: npm
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
env:
|
|
||||||
CI: true
|
|
||||||
|
|
||||||
- name: Run update-languages-json.js script
|
|
||||||
run: npm run generate-langs-json
|
|
||||||
|
|
||||||
- name: Create Pull Request if upstream language file is changed
|
|
||||||
uses: peter-evans/create-pull-request@v4
|
|
||||||
with:
|
|
||||||
commit-message: "refactor: update languages JSON"
|
|
||||||
branch: "update_langs/patch"
|
|
||||||
delete-branch: true
|
|
||||||
title: Update languages JSON
|
|
||||||
body:
|
|
||||||
"The
|
|
||||||
[update-langs](https://github.com/anuraghazra/github-readme-stats/actions/workflows/update-langs.yaml)
|
|
||||||
action found new/updated languages in the [upstream languages JSON
|
|
||||||
file](https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml)."
|
|
||||||
labels: "ci, lang-card"
|
|
||||||
+43
-184
@@ -80,9 +80,6 @@ Visit <https://indiafightscorona.giveindia.org> and make a small donation to hel
|
|||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
- [Getting Started](#getting-started)
|
|
||||||
- [GitHub Actions](#github-actions-recommended)
|
|
||||||
- [Community Deployment](#community-deployment)
|
|
||||||
- [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)
|
||||||
@@ -96,142 +93,18 @@ Visit <https://indiafightscorona.giveindia.org> and make a small donation to hel
|
|||||||
- [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)
|
- [Keep your fork up to date](#keep-your-fork-up-to-date)
|
||||||
|
|
||||||
# Getting Started
|
|
||||||
|
|
||||||
## GitHub Actions (Recommended)
|
# GitHub Stats Card
|
||||||
|
|
||||||
Using GitHub Actions over the community deployment is recommended because it provides more accurate stats and 100% uptime. Simply copy-paste the following into the `/.github/workflows/grs.yml` file in your README repo (`USERNAME/USERNAME`).
|
Copy-paste this into your markdown content, and that is it. Simple!
|
||||||
|
|
||||||
```yml
|
Change the `?username=` value to your GitHub username.
|
||||||
name: GitHub Readme Stats
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: 0 * * * *
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
grs:
|
|
||||||
permissions: write-all
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Generate Stats
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Generate Stats
|
|
||||||
uses: Zo-Bro-23/grs-action@v1
|
|
||||||
id: generate
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
options: 'username=${{ github.repository_owner }}'
|
|
||||||
- name: Push
|
|
||||||
uses: crazy-max/ghaction-github-pages@v3
|
|
||||||
with:
|
|
||||||
target_branch: grs
|
|
||||||
build_dir: grs
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
```
|
|
||||||
|
|
||||||
If you want to generate multiple stats, such as top languages, wakatime, etc, you can use the following code.
|
|
||||||
|
|
||||||
```yml
|
|
||||||
name: GitHub Readme Stats
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: 0 * * * *
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
stats:
|
|
||||||
permissions: write-all
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Stats Card
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Generate Stats
|
|
||||||
uses: Zo-Bro-23/grs-action@v1
|
|
||||||
id: generate
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
options: 'username=${{ github.repository_owner }}'
|
|
||||||
- uses: actions/upload-artifact@master
|
|
||||||
with:
|
|
||||||
name: stats
|
|
||||||
path: grs
|
|
||||||
top-langs:
|
|
||||||
permissions: write-all
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Top Languages Card
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Generate Top Languages
|
|
||||||
uses: Zo-Bro-23/grs-action@v1
|
|
||||||
id: generate
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
options: 'username=${{ github.repository_owner }}'
|
|
||||||
card: 'top-langs'
|
|
||||||
- uses: actions/upload-artifact@master
|
|
||||||
with:
|
|
||||||
name: top-langs
|
|
||||||
path: grs
|
|
||||||
push:
|
|
||||||
permissions: write-all
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Push
|
|
||||||
needs: [stats, top-langs]
|
|
||||||
steps:
|
|
||||||
- uses: actions/download-artifact@master
|
|
||||||
with:
|
|
||||||
name: stats
|
|
||||||
path: grs
|
|
||||||
- uses: actions/download-artifact@master
|
|
||||||
with:
|
|
||||||
name: top-langs
|
|
||||||
path: grs
|
|
||||||
- name: Push
|
|
||||||
uses: crazy-max/ghaction-github-pages@v3
|
|
||||||
with:
|
|
||||||
target_branch: grs
|
|
||||||
build_dir: grs
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
```
|
|
||||||
|
|
||||||
You can now include the stats in your README by using the following markdown code (replace `USERNAME` with your GitHub username).
|
|
||||||
|
|
||||||
```md
|
|
||||||
[](https://github.com/anuraghazra/github-readme-stats)
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also enable GitHub Pages on the `grs` branch (root) and use that URL (replace `GH_PAGES_URL` with your GitHub Pages URL).
|
|
||||||
|
|
||||||
```md
|
|
||||||
[](https://github.com/anuraghazra/github-readme-stats)
|
|
||||||
```
|
|
||||||
|
|
||||||
The GitHub Action accepts the following inputs:
|
|
||||||
- `token` - `REQUIRED` - Your GitHub Personal Access Token (PAT), or any other token that GitHub accepts.
|
|
||||||
- `options` - `REQUIRED` - Any customization options. More below. (This is the same as the query string you would pass to the API if using the community deployment)
|
|
||||||
- `card` - The type of card to generate. `repo`, `top-langs`, or `wakatime` (leave empty for stats card).
|
|
||||||
- `path` - Output path for SVG file. Relative path; include filename with `.svg`.
|
|
||||||
- `fetch_multipage` - Experimental feature that gives more accurate stats if you have >100 repos.
|
|
||||||
|
|
||||||
## Community Deployment
|
|
||||||
Copy-paste the following markdown code. Change the `?username=` value to your GitHub username.
|
|
||||||
|
|
||||||
```md
|
```md
|
||||||
[](https://github.com/anuraghazra/github-readme-stats)
|
[](https://github.com/anuraghazra/github-readme-stats)
|
||||||
```
|
```
|
||||||
|
|
||||||
Other cards can be included by using `/api/top-langs`, `/api/pin`, or `/api/wakatime`. More customization options can be added by using query parameters (`&option1=value1&option2=value2&...`).
|
|
||||||
|
|
||||||
# GitHub Stats Card
|
|
||||||
|
|
||||||
> **Note**
|
> **Note**
|
||||||
> 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).
|
> 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).
|
||||||
|
|
||||||
@@ -242,7 +115,7 @@ You can pass a query parameter `&hide=` to hide any specific stats with comma-se
|
|||||||
> Options: `&hide=stars,commits,prs,issues,contribs`
|
> Options: `&hide=stars,commits,prs,issues,contribs`
|
||||||
|
|
||||||
```md
|
```md
|
||||||
?username=anuraghazra&hide=contribs,prs
|

|
||||||
```
|
```
|
||||||
|
|
||||||
### Adding private contributions count to total commits count
|
### Adding private contributions count to total commits count
|
||||||
@@ -255,7 +128,7 @@ You can add the count of all your private contributions to the total commits cou
|
|||||||
> Options: `&count_private=true`
|
> Options: `&count_private=true`
|
||||||
|
|
||||||
```md
|
```md
|
||||||
?username=anuraghazra&count_private=true
|

|
||||||
```
|
```
|
||||||
|
|
||||||
### Showing icons
|
### Showing icons
|
||||||
@@ -263,7 +136,7 @@ You can add the count of all your private contributions to the total commits cou
|
|||||||
To enable icons, you can pass `show_icons=true` in the query param, like so:
|
To enable icons, you can pass `show_icons=true` in the query param, like so:
|
||||||
|
|
||||||
```md
|
```md
|
||||||
?username=anuraghazra&show_icons=true
|

|
||||||
```
|
```
|
||||||
|
|
||||||
### Themes
|
### Themes
|
||||||
@@ -273,7 +146,7 @@ With inbuilt themes, you can customize the look of the card without doing any [m
|
|||||||
Use `&theme=THEME_NAME` parameter like so :
|
Use `&theme=THEME_NAME` parameter like so :
|
||||||
|
|
||||||
```md
|
```md
|
||||||
?username=anuraghazra&show_icons=true&theme=radical
|

|
||||||
```
|
```
|
||||||
|
|
||||||
#### All inbuilt themes
|
#### All inbuilt themes
|
||||||
@@ -296,7 +169,7 @@ Since GitHub will re-upload the cards and serve them from their [CDN](https://do
|
|||||||
We have included a `transparent` theme that has a transparent background. This theme is optimized to look good on GitHub's dark and light default themes. You can enable this theme using the `&theme=transparent` parameter like so:
|
We have included a `transparent` theme that has a transparent background. This theme is optimized to look good on GitHub's dark and light default themes. You can enable this theme using the `&theme=transparent` parameter like so:
|
||||||
|
|
||||||
```md
|
```md
|
||||||
?username=anuraghazra&show_icons=true&theme=transparent
|

|
||||||
```
|
```
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -311,7 +184,7 @@ We have included a `transparent` theme that has a transparent background. This t
|
|||||||
You can use the `bg_color` parameter to make any of [the available themes](./themes/README.md) transparent. This is done by setting the `bg_color` to a colour with a transparent alpha channel (i.e. `bg_color=00000000`):
|
You can use the `bg_color` parameter to make any of [the available themes](./themes/README.md) transparent. This is done by setting the `bg_color` to a colour with a transparent alpha channel (i.e. `bg_color=00000000`):
|
||||||
|
|
||||||
```md
|
```md
|
||||||
?username=anuraghazra&show_icons=true&bg_color=00000000
|

|
||||||
```
|
```
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -326,8 +199,8 @@ You can use the `bg_color` parameter to make any of [the available themes](./the
|
|||||||
You can use [GitHub's theme context](https://github.blog/changelog/2021-11-24-specify-theme-context-for-images-in-markdown/) tags to switch the theme based on the user GitHub theme automatically. This is done by appending `#gh-dark-mode-only` or `#gh-light-mode-only` to the end of an image URL. This tag will define whether the image specified in the markdown is only shown to viewers using a light or a dark GitHub theme:
|
You can use [GitHub's theme context](https://github.blog/changelog/2021-11-24-specify-theme-context-for-images-in-markdown/) tags to switch the theme based on the user GitHub theme automatically. This is done by appending `#gh-dark-mode-only` or `#gh-light-mode-only` to the end of an image URL. This tag will define whether the image specified in the markdown is only shown to viewers using a light or a dark GitHub theme:
|
||||||
|
|
||||||
```md
|
```md
|
||||||
[](https://github.com/anuraghazra/github-readme-stats#gh-dark-mode-only)
|
[](https://github.com/anuraghazra/github-readme-stats#gh-dark-mode-only)
|
||||||
[](https://github.com/anuraghazra/github-readme-stats#gh-light-mode-only)
|
[](https://github.com/anuraghazra/github-readme-stats#gh-light-mode-only)
|
||||||
```
|
```
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -345,14 +218,14 @@ You can use [GitHub's new media feature](https://github.blog/changelog/2022-05-1
|
|||||||
```html
|
```html
|
||||||
<picture>
|
<picture>
|
||||||
<source
|
<source
|
||||||
srcset="https://DARK_SVG_URL/"
|
srcset="https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark"
|
||||||
media="(prefers-color-scheme: dark)"
|
media="(prefers-color-scheme: dark)"
|
||||||
/>
|
/>
|
||||||
<source
|
<source
|
||||||
srcset="https://LIGHT_SVG_URL/"
|
srcset="https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true"
|
||||||
media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
|
media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
|
||||||
/>
|
/>
|
||||||
<img src="https://DEFAULT_SVG_URL/" />
|
<img src="https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true" />
|
||||||
</picture>
|
</picture>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -375,7 +248,7 @@ You can use [GitHub's new media feature](https://github.blog/changelog/2022-05-1
|
|||||||
|
|
||||||
### Customization
|
### Customization
|
||||||
|
|
||||||
You can customize the appearance of your cards however you wish with these parameters.
|
You can customize the appearance of your `Stats Card` or `Repo Card` however you wish with URL parameters.
|
||||||
|
|
||||||
#### Common Options
|
#### Common Options
|
||||||
|
|
||||||
@@ -386,12 +259,12 @@ You can customize the appearance of your cards however you wish with these param
|
|||||||
- `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: 14400, 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**
|
> **Warning**
|
||||||
> 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. This is why we recommend using our GitHub Action :)
|
> 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
|
||||||
|
|
||||||
@@ -431,8 +304,6 @@ You can provide multiple comma-separated values in the bg_color option to render
|
|||||||
- `langs_count` - Show more languages on the card, between 1-10 _(number)_. Default `5`.
|
- `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)`.
|
- `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`.
|
- `custom_title` - Sets a custom title for the card _(string)_. Default `Most Used Languages`.
|
||||||
- `disable_animations` - Disables all animations in the card _(boolean)_. Default: `false`.
|
|
||||||
- `hide_progress` - It uses the compact layout option, hides percentages, and removes the bars. Default: `false`.
|
|
||||||
|
|
||||||
> **Warning**
|
> **Warning**
|
||||||
> Language names should be URI-escaped, as specified in [Percent Encoding](https://en.wikipedia.org/wiki/Percent-encoding)
|
> Language names should be URI-escaped, as specified in [Percent Encoding](https://en.wikipedia.org/wiki/Percent-encoding)
|
||||||
@@ -461,7 +332,13 @@ Yay! You are no longer limited to 6 pinned repositories.
|
|||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
Use the `api/pin` endpoint with the API or the `card: 'repo'` option with the GitHub Action.
|
Copy-paste this code into your readme and change the links.
|
||||||
|
|
||||||
|
Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`
|
||||||
|
|
||||||
|
```md
|
||||||
|
[](https://github.com/anuraghazra/github-readme-stats)
|
||||||
|
```
|
||||||
|
|
||||||
### Demo
|
### Demo
|
||||||
|
|
||||||
@@ -476,18 +353,24 @@ 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 skill level or anything like that; it's a GitHub metric to determine which languages have the most code on GitHub.
|
> 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
|
||||||
|
|
||||||
Use the `api/top-langs` endpoint with the API or the `card: 'top-langs'` option with the GitHub Action.
|
Copy-paste this code into your readme and change the links.
|
||||||
|
|
||||||
|
Endpoint: `api/top-langs?username=anuraghazra`
|
||||||
|
|
||||||
|
```md
|
||||||
|
[](https://github.com/anuraghazra/github-readme-stats)
|
||||||
|
```
|
||||||
|
|
||||||
### Exclude individual repositories
|
### Exclude individual repositories
|
||||||
|
|
||||||
You can use the `&exclude_repo=repo1,repo2` parameter to exclude individual repositories.
|
You can use the `&exclude_repo=repo1,repo2` parameter to exclude individual repositories.
|
||||||
|
|
||||||
```md
|
```md
|
||||||
?username=anuraghazra&exclude_repo=github-readme-stats,anuraghazra.github.io
|
[](https://github.com/anuraghazra/github-readme-stats)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Hide individual languages
|
### Hide individual languages
|
||||||
@@ -495,7 +378,7 @@ You can use the `&exclude_repo=repo1,repo2` parameter to exclude individual repo
|
|||||||
You can use `&hide=language1,language2` parameter to hide individual languages.
|
You can use `&hide=language1,language2` parameter to hide individual languages.
|
||||||
|
|
||||||
```md
|
```md
|
||||||
?username=anuraghazra&hide=javascript,html
|
[](https://github.com/anuraghazra/github-readme-stats)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Show more languages
|
### Show more languages
|
||||||
@@ -503,7 +386,7 @@ You can use `&hide=language1,language2` parameter to hide individual languages.
|
|||||||
You can use the `&langs_count=` option to increase or decrease the number of languages shown on the card. Valid values are integers between 1 and 10 (inclusive), and the default is 5.
|
You can use the `&langs_count=` option to increase or decrease the number of languages shown on the card. Valid values are integers between 1 and 10 (inclusive), and the default is 5.
|
||||||
|
|
||||||
```md
|
```md
|
||||||
?username=anuraghazra&langs_count=8
|
[](https://github.com/anuraghazra/github-readme-stats)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compact Language Card Layout
|
### Compact Language Card Layout
|
||||||
@@ -511,15 +394,7 @@ You can use the `&langs_count=` option to increase or decrease the number of lan
|
|||||||
You can use the `&layout=compact` option to change the card design.
|
You can use the `&layout=compact` option to change the card design.
|
||||||
|
|
||||||
```md
|
```md
|
||||||
?username=anuraghazra&layout=compact
|
[](https://github.com/anuraghazra/github-readme-stats)
|
||||||
```
|
|
||||||
|
|
||||||
### Hide Progress Bars
|
|
||||||
|
|
||||||
You can use the `&hide_progress=true` option to hide the percentages and the progress bars (layout will be automatically set to `compact`).
|
|
||||||
|
|
||||||
```md
|
|
||||||
?username=anuraghazra&hide_progress=true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Demo
|
### Demo
|
||||||
@@ -530,33 +405,17 @@ You can use the `&hide_progress=true` option to hide the percentages and the pro
|
|||||||
|
|
||||||
[](https://github.com/anuraghazra/github-readme-stats)
|
[](https://github.com/anuraghazra/github-readme-stats)
|
||||||
|
|
||||||
- Hidden progress bars
|
|
||||||
|
|
||||||
[](https://github.com/anuraghazra/github-readme-stats)
|
|
||||||
|
|
||||||
# Wakatime Week Stats
|
# Wakatime Week Stats
|
||||||
|
|
||||||
### Usage
|
|
||||||
|
|
||||||
Use the `api/wkatime` endpoint with the API or the `card: 'wakatime'` option with the GitHub Action.
|
|
||||||
|
|
||||||
Change the `?username=` value to your [Wakatime](https://wakatime.com) username.
|
Change the `?username=` value to your [Wakatime](https://wakatime.com) username.
|
||||||
|
|
||||||
```md
|
```md
|
||||||
?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.
|
||||||
|
|
||||||
### Compact Wakatime Card Layout
|
|
||||||
|
|
||||||
You can use the `&layout=compact` option to change the card design.
|
|
||||||
|
|
||||||
```md
|
|
||||||
?username=willianrod&layout=compact
|
|
||||||
```
|
|
||||||
|
|
||||||
### Demo
|
### Demo
|
||||||
|
|
||||||
[](https://github.com/anuraghazra/github-readme-stats)
|
[](https://github.com/anuraghazra/github-readme-stats)
|
||||||
@@ -629,16 +488,16 @@ By default, GitHub does not lay out the cards side by side. To do that, you can
|
|||||||
|
|
||||||
```html
|
```html
|
||||||
<a href="https://github.com/anuraghazra/github-readme-stats">
|
<a href="https://github.com/anuraghazra/github-readme-stats">
|
||||||
<img align="center" src="https://SVG_URL/" />
|
<img align="center" src="https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/anuraghazra/convoychat">
|
<a href="https://github.com/anuraghazra/convoychat">
|
||||||
<img align="center" src="https://SVG_URL/" />
|
<img align="center" src="https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=convoychat" />
|
||||||
</a>
|
</a>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Deploy your own Vercel instance
|
## Deploy on your own Vercel instance
|
||||||
|
|
||||||
#### :film_projector: [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**
|
> **Warning**
|
||||||
> 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).
|
> 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).
|
||||||
@@ -675,7 +534,7 @@ Since the GitHub API only allows 5k requests per hour, my `https://github-readme
|
|||||||
|
|
||||||
### Keep your fork up to date
|
### Keep your fork up to date
|
||||||
|
|
||||||
You can keep your fork, and thus your private Vercel instance up to date with the upstream using GitHub's [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.
|
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
|
||||||
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file Contains a simple cloud function that can be used to check which PATs are no
|
|
||||||
* longer working. It returns a list of valid PATs, expired PATs and PATs with errors.
|
|
||||||
*
|
|
||||||
* @description This function is currently rate limited to 1 request per 10 minutes.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { logger, request, dateDiff } from "../../src/common/utils.js";
|
|
||||||
export const RATE_LIMIT_SECONDS = 60 * 5; // 1 request per 10 minutes
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple uptime check fetcher for the PATs.
|
|
||||||
*
|
|
||||||
* @param {import('axios').AxiosRequestHeaders} variables
|
|
||||||
* @param {string} token
|
|
||||||
*/
|
|
||||||
const uptimeFetcher = (variables, token) => {
|
|
||||||
return request(
|
|
||||||
{
|
|
||||||
query: `
|
|
||||||
query {
|
|
||||||
rateLimit {
|
|
||||||
remaining
|
|
||||||
resetAt
|
|
||||||
},
|
|
||||||
}`,
|
|
||||||
variables,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Authorization: `bearer ${token}`,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAllPATs = () => {
|
|
||||||
return Object.keys(process.env).filter((key) => /PAT_\d*$/.exec(key));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether any of the PATs is expired.
|
|
||||||
*/
|
|
||||||
const getPATInfo = async (fetcher, variables) => {
|
|
||||||
const details = {};
|
|
||||||
const PATs = getAllPATs();
|
|
||||||
|
|
||||||
for (const pat of PATs) {
|
|
||||||
try {
|
|
||||||
const response = await fetcher(variables, process.env[pat]);
|
|
||||||
const errors = response.data.errors;
|
|
||||||
const hasErrors = Boolean(errors);
|
|
||||||
const errorType = errors?.[0]?.type;
|
|
||||||
const isRateLimited =
|
|
||||||
(hasErrors && errorType === "RATE_LIMITED") ||
|
|
||||||
response.data.data?.rateLimit?.remaining === 0;
|
|
||||||
|
|
||||||
// Store PATs with errors.
|
|
||||||
if (hasErrors && errorType !== "RATE_LIMITED") {
|
|
||||||
details[pat] = {
|
|
||||||
status: "error",
|
|
||||||
error: {
|
|
||||||
type: errors[0].type,
|
|
||||||
message: errors[0].message,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
continue;
|
|
||||||
} else if (isRateLimited) {
|
|
||||||
const date1 = new Date();
|
|
||||||
const date2 = new Date(response.data?.data?.rateLimit?.resetAt);
|
|
||||||
details[pat] = {
|
|
||||||
status: "exhausted",
|
|
||||||
remaining: 0,
|
|
||||||
resetIn: dateDiff(date2, date1) + " minutes",
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
details[pat] = {
|
|
||||||
status: "valid",
|
|
||||||
remaining: response.data.data.rateLimit.remaining,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Store the PAT if it is expired.
|
|
||||||
const errorMessage = err.response?.data?.message?.toLowerCase();
|
|
||||||
if (errorMessage === "bad credentials") {
|
|
||||||
details[pat] = {
|
|
||||||
status: "expired",
|
|
||||||
};
|
|
||||||
} else if (errorMessage === "sorry. your account was suspended.") {
|
|
||||||
details[pat] = {
|
|
||||||
status: "suspended",
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterPATsByStatus = (status) => {
|
|
||||||
return Object.keys(details).filter((pat) => details[pat].status === status);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sortedDetails = Object.keys(details)
|
|
||||||
.sort()
|
|
||||||
.reduce((obj, key) => {
|
|
||||||
obj[key] = details[key];
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return {
|
|
||||||
validPATs: filterPATsByStatus("valid"),
|
|
||||||
expiredPATs: filterPATsByStatus("expired"),
|
|
||||||
exhaustedPATs: filterPATsByStatus("exhausted"),
|
|
||||||
suspendedPATs: filterPATsByStatus("suspended"),
|
|
||||||
errorPATs: filterPATsByStatus("error"),
|
|
||||||
details: sortedDetails,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cloud function that returns information about the used PATs.
|
|
||||||
*/
|
|
||||||
export default async (_, res) => {
|
|
||||||
res.setHeader("Content-Type", "application/json");
|
|
||||||
try {
|
|
||||||
// Add header to prevent abuse.
|
|
||||||
const PATsInfo = await getPATInfo(uptimeFetcher, {});
|
|
||||||
if (PATsInfo) {
|
|
||||||
res.setHeader(
|
|
||||||
"Cache-Control",
|
|
||||||
`max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
res.send(JSON.stringify(PATsInfo, null, 2));
|
|
||||||
} catch (err) {
|
|
||||||
// Throw error if something went wrong.
|
|
||||||
logger.error(err);
|
|
||||||
res.setHeader("Cache-Control", "no-store");
|
|
||||||
res.send("Something went wrong: " + err.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file Contains a simple cloud function that can be used to check if the PATs are still
|
|
||||||
* functional.
|
|
||||||
*
|
|
||||||
* @description This function is currently rate limited to 1 request per 10 minutes.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import retryer from "../../src/common/retryer.js";
|
|
||||||
import { logger, request } from "../../src/common/utils.js";
|
|
||||||
|
|
||||||
export const RATE_LIMIT_SECONDS = 60 * 10; // 1 request per 10 minutes
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple uptime check fetcher for the PATs.
|
|
||||||
*
|
|
||||||
* @param {import('axios').AxiosRequestHeaders} variables
|
|
||||||
* @param {string} token
|
|
||||||
*/
|
|
||||||
const uptimeFetcher = (variables, token) => {
|
|
||||||
return request(
|
|
||||||
{
|
|
||||||
query: `
|
|
||||||
query {
|
|
||||||
rateLimit {
|
|
||||||
remaining
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
variables,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Authorization: `bearer ${token}`,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates Json response that can be used for shields.io dynamic card generation.
|
|
||||||
*
|
|
||||||
* @param {*} up Whether the PATs are up or not.
|
|
||||||
* @returns Dynamic shields.io JSON response object.
|
|
||||||
*
|
|
||||||
* @see https://shields.io/endpoint.
|
|
||||||
*/
|
|
||||||
const shieldsUptimeBadge = (up) => {
|
|
||||||
const schemaVersion = 1;
|
|
||||||
const isError = true;
|
|
||||||
const label = "Public Instance";
|
|
||||||
const message = up ? "up" : "down";
|
|
||||||
const color = up ? "brightgreen" : "red";
|
|
||||||
return {
|
|
||||||
schemaVersion,
|
|
||||||
label,
|
|
||||||
message,
|
|
||||||
color,
|
|
||||||
isError,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cloud function that returns whether the PATs are still functional.
|
|
||||||
*/
|
|
||||||
export default async (req, res) => {
|
|
||||||
let { type } = req.query;
|
|
||||||
type = type ? type.toLowerCase() : "boolean";
|
|
||||||
|
|
||||||
res.setHeader("Content-Type", "application/json");
|
|
||||||
|
|
||||||
try {
|
|
||||||
let PATsValid = true;
|
|
||||||
try {
|
|
||||||
await retryer(uptimeFetcher, {});
|
|
||||||
} catch (err) {
|
|
||||||
PATsValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PATsValid) {
|
|
||||||
res.setHeader(
|
|
||||||
"Cache-Control",
|
|
||||||
`max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
res.setHeader("Cache-Control", "no-store");
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case "shields":
|
|
||||||
res.send(shieldsUptimeBadge(PATsValid));
|
|
||||||
break;
|
|
||||||
case "json":
|
|
||||||
res.send({ up: PATsValid });
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
res.send(PATsValid);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Return fail boolean if something went wrong.
|
|
||||||
logger.error(err);
|
|
||||||
res.setHeader("Cache-Control", "no-store");
|
|
||||||
res.send("Something went wrong: " + err.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -29,8 +29,6 @@ export default async (req, res) => {
|
|||||||
locale,
|
locale,
|
||||||
border_radius,
|
border_radius,
|
||||||
border_color,
|
border_color,
|
||||||
disable_animations,
|
|
||||||
hide_progress,
|
|
||||||
} = req.query;
|
} = req.query;
|
||||||
res.setHeader("Content-Type", "image/svg+xml");
|
res.setHeader("Content-Type", "image/svg+xml");
|
||||||
|
|
||||||
@@ -77,8 +75,6 @@ export default async (req, res) => {
|
|||||||
border_radius,
|
border_radius,
|
||||||
border_color,
|
border_color,
|
||||||
locale: locale ? locale.toLowerCase() : null,
|
locale: locale ? locale.toLowerCase() : null,
|
||||||
disable_animations: parseBoolean(disable_animations),
|
|
||||||
hide_progress: parseBoolean(hide_progress),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Generated
+1
-1
@@ -22,7 +22,7 @@
|
|||||||
"@testing-library/dom": "^8.17.1",
|
"@testing-library/dom": "^8.17.1",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@uppercod/css-to-object": "^1.1.1",
|
"@uppercod/css-to-object": "^1.1.1",
|
||||||
"axios-mock-adapter": "^1.21.2",
|
"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": "^8.0.0",
|
||||||
|
|||||||
+1
-1
@@ -39,7 +39,7 @@
|
|||||||
"@testing-library/dom": "^8.17.1",
|
"@testing-library/dom": "^8.17.1",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@uppercod/css-to-object": "^1.1.1",
|
"@uppercod/css-to-object": "^1.1.1",
|
||||||
"axios-mock-adapter": "^1.21.2",
|
"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": "^8.0.0",
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ git config --global user.name "GitHub Readme Stats Bot"
|
|||||||
git branch -d $BRANCH_NAME || true
|
git branch -d $BRANCH_NAME || true
|
||||||
git checkout -b $BRANCH_NAME
|
git checkout -b $BRANCH_NAME
|
||||||
git add --all
|
git add --all
|
||||||
git commit --no-verify --message "docs(theme): Auto update theme readme"
|
git commit --message "docs(theme): Auto update theme readme" || exit 0
|
||||||
git remote add origin-$BRANCH_NAME https://${PERSONAL_TOKEN}@github.com/${GH_REPO}.git
|
git remote add origin-$BRANCH_NAME https://${PERSONAL_TOKEN}@github.com/${GH_REPO}.git
|
||||||
git push --force --quiet --set-upstream origin-$BRANCH_NAME $BRANCH_NAME
|
git push --force --quiet --set-upstream origin-$BRANCH_NAME $BRANCH_NAME
|
||||||
|
|||||||
+12
-26
@@ -9,14 +9,9 @@ import {
|
|||||||
kFormatter,
|
kFormatter,
|
||||||
measureText,
|
measureText,
|
||||||
} from "../common/utils.js";
|
} from "../common/utils.js";
|
||||||
import { getStyles } from "../getStyles.js";
|
import { getStyles } from "../common/getStyles.js";
|
||||||
import { statCardLocales } from "../translations.js";
|
import { statCardLocales } from "../translations.js";
|
||||||
|
|
||||||
const CARD_MIN_WIDTH = 287;
|
|
||||||
const CARD_DEFAULT_WIDTH = 287;
|
|
||||||
const RANK_CARD_MIN_WIDTH = 420;
|
|
||||||
const RANK_CARD_DEFAULT_WIDTH = 450;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a stats card text item.
|
* Create a stats card text item.
|
||||||
*
|
*
|
||||||
@@ -223,17 +218,11 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
|||||||
When hide_rank=false, the minimum card_width is 340 px + the icon width (if show_icons=true).
|
When hide_rank=false, the minimum card_width is 340 px + the icon width (if show_icons=true).
|
||||||
Numbers are picked by looking at existing dimensions on production.
|
Numbers are picked by looking at existing dimensions on production.
|
||||||
*/
|
*/
|
||||||
const iconWidth = show_icons ? 16 + /* padding */ 1 : 0;
|
const iconWidth = show_icons ? 16 : 0;
|
||||||
const minCardWidth =
|
const minCardWidth = hide_rank
|
||||||
(hide_rank
|
? clampValue(50 /* padding */ + calculateTextWidth() * 2, 270, Infinity)
|
||||||
? clampValue(
|
: 340 + iconWidth;
|
||||||
50 /* padding */ + calculateTextWidth() * 2,
|
const defaultCardWidth = hide_rank ? 270 : 495;
|
||||||
CARD_MIN_WIDTH,
|
|
||||||
Infinity,
|
|
||||||
)
|
|
||||||
: RANK_CARD_MIN_WIDTH) + iconWidth;
|
|
||||||
const defaultCardWidth =
|
|
||||||
(hide_rank ? CARD_DEFAULT_WIDTH : RANK_CARD_DEFAULT_WIDTH) + iconWidth;
|
|
||||||
let width = isNaN(card_width) ? defaultCardWidth : card_width;
|
let width = isNaN(card_width) ? defaultCardWidth : card_width;
|
||||||
if (width < minCardWidth) {
|
if (width < minCardWidth) {
|
||||||
width = minCardWidth;
|
width = minCardWidth;
|
||||||
@@ -262,21 +251,18 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the right rank circle translation values such that the rank circle
|
* Calculates the right rank circle translation values such that the rank circle
|
||||||
* keeps respecting the following padding:
|
* keeps respecting the padding.
|
||||||
*
|
*
|
||||||
* width > RANK_CARD_DEFAULT_WIDTH: The default right padding of 70 px will be used.
|
* width > 450: The default left padding of 50 px will be used.
|
||||||
* width < RANK_CARD_DEFAULT_WIDTH: The left and right padding will be enlarged
|
* width < 450: The left and right padding will shrink equally.
|
||||||
* equally from a certain minimum at RANK_CARD_MIN_WIDTH.
|
|
||||||
*
|
*
|
||||||
* @returns {number} - Rank circle translation value.
|
* @returns {number} - Rank circle translation value.
|
||||||
*/
|
*/
|
||||||
const calculateRankXTranslation = () => {
|
const calculateRankXTranslation = () => {
|
||||||
const minXTranslation = RANK_CARD_MIN_WIDTH + iconWidth - 70;
|
if (width < 450) {
|
||||||
if (width > RANK_CARD_DEFAULT_WIDTH) {
|
return width - 95 + (45 * (450 - 340)) / 110;
|
||||||
const xMaxExpansion = minXTranslation + (450 - minCardWidth) / 2;
|
|
||||||
return xMaxExpansion + width - RANK_CARD_DEFAULT_WIDTH;
|
|
||||||
} else {
|
} else {
|
||||||
return minXTranslation + (width - minCardWidth) / 2;
|
return width - 95;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -39,57 +39,49 @@ const getLongestLang = (arr) =>
|
|||||||
* Creates a node to display usage of a programming language in percentage
|
* Creates a node to display usage of a programming language in percentage
|
||||||
* using text and a horizontal progress bar.
|
* using text and a horizontal progress bar.
|
||||||
*
|
*
|
||||||
* @param {object} props Function properties.
|
* @param {object[]} props Function properties.
|
||||||
* @param {number} props.width The card width
|
* @param {number} props.width The card width
|
||||||
* @param {string} props.name Name of the programming language.
|
* @param {string} props.name Name of the programming language.
|
||||||
* @param {string} props.color Color of the programming language.
|
* @param {string} props.color Color of the programming language.
|
||||||
* @param {string} props.progress Usage of the programming language in percentage.
|
* @param {string} props.progress Usage of the programming language in percentage.
|
||||||
* @param {number} props.index Index of the programming language.
|
|
||||||
* @returns {string} Programming language SVG node.
|
* @returns {string} Programming language SVG node.
|
||||||
*/
|
*/
|
||||||
const createProgressTextNode = ({ width, color, name, progress, index }) => {
|
const createProgressTextNode = ({ width, color, name, progress }) => {
|
||||||
const staggerDelay = (index + 3) * 150;
|
|
||||||
const paddingRight = 95;
|
const paddingRight = 95;
|
||||||
const progressTextX = width - paddingRight + 10;
|
const progressTextX = width - paddingRight + 10;
|
||||||
const progressWidth = width - paddingRight;
|
const progressWidth = width - paddingRight;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
|
<text data-testid="lang-name" x="2" y="15" class="lang-name">${name}</text>
|
||||||
<text data-testid="lang-name" x="2" y="15" class="lang-name">${name}</text>
|
<text x="${progressTextX}" y="34" class="lang-name">${progress}%</text>
|
||||||
<text x="${progressTextX}" y="34" class="lang-name">${progress}%</text>
|
${createProgressNode({
|
||||||
${createProgressNode({
|
x: 0,
|
||||||
x: 0,
|
y: 25,
|
||||||
y: 25,
|
color,
|
||||||
color,
|
width: progressWidth,
|
||||||
width: progressWidth,
|
progress,
|
||||||
progress,
|
progressBarBackgroundColor: "#ddd",
|
||||||
progressBarBackgroundColor: "#ddd",
|
})}
|
||||||
delay: staggerDelay + 300,
|
|
||||||
})}
|
|
||||||
</g>
|
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a text only node to display usage of a programming language in percentage.
|
* Creates a text only node to display usage of a programming language in percentage.
|
||||||
*
|
*
|
||||||
* @param {object} props Function properties.
|
* @param {object[]} props Function properties.
|
||||||
* @param {Lang} props.lang Programming language object.
|
* @param {Lang} props.lang Programming language object.
|
||||||
* @param {number} props.totalSize Total size of all languages.
|
* @param {number} props.totalSize Total size of all languages.
|
||||||
* @param {boolean} props.hideProgress Whether to hide percentage.
|
|
||||||
* @param {number} props.index Index of the programming language.
|
|
||||||
* @returns {string} Compact layout programming language SVG node.
|
* @returns {string} Compact layout programming language SVG node.
|
||||||
*/
|
*/
|
||||||
const createCompactLangNode = ({ lang, totalSize, hideProgress, index }) => {
|
const createCompactLangNode = ({ lang, totalSize }) => {
|
||||||
const percentage = ((lang.size / totalSize) * 100).toFixed(2);
|
const percentage = ((lang.size / totalSize) * 100).toFixed(2);
|
||||||
const staggerDelay = (index + 3) * 150;
|
|
||||||
const color = lang.color || "#858585";
|
const color = lang.color || "#858585";
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
|
<g>
|
||||||
<circle cx="5" cy="6" r="5" fill="${color}" />
|
<circle cx="5" cy="6" r="5" fill="${color}" />
|
||||||
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
||||||
${lang.name} ${hideProgress ? "" : percentage + "%"}
|
${lang.name} ${percentage}%
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
`;
|
`;
|
||||||
@@ -101,10 +93,9 @@ const createCompactLangNode = ({ lang, totalSize, hideProgress, index }) => {
|
|||||||
* @param {object[]} props Function properties.
|
* @param {object[]} props Function properties.
|
||||||
* @param {Lang[]} props.langs Array of programming languages.
|
* @param {Lang[]} props.langs Array of programming languages.
|
||||||
* @param {number} props.totalSize Total size of all languages.
|
* @param {number} props.totalSize Total size of all languages.
|
||||||
* @param {boolean} props.hideProgress Whether to hide percentage.
|
|
||||||
* @returns {string} Programming languages SVG node.
|
* @returns {string} Programming languages SVG node.
|
||||||
*/
|
*/
|
||||||
const createLanguageTextNode = ({ langs, totalSize, hideProgress }) => {
|
const createLanguageTextNode = ({ langs, totalSize }) => {
|
||||||
const longestLang = getLongestLang(langs);
|
const longestLang = getLongestLang(langs);
|
||||||
const chunked = chunkArray(langs, langs.length / 2);
|
const chunked = chunkArray(langs, langs.length / 2);
|
||||||
const layouts = chunked.map((array) => {
|
const layouts = chunked.map((array) => {
|
||||||
@@ -113,7 +104,7 @@ const createLanguageTextNode = ({ langs, totalSize, hideProgress }) => {
|
|||||||
createCompactLangNode({
|
createCompactLangNode({
|
||||||
lang,
|
lang,
|
||||||
totalSize,
|
totalSize,
|
||||||
hideProgress,
|
// @ts-ignore
|
||||||
index,
|
index,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -143,13 +134,12 @@ const createLanguageTextNode = ({ langs, totalSize, hideProgress }) => {
|
|||||||
*/
|
*/
|
||||||
const renderNormalLayout = (langs, width, totalLanguageSize) => {
|
const renderNormalLayout = (langs, width, totalLanguageSize) => {
|
||||||
return flexLayout({
|
return flexLayout({
|
||||||
items: langs.map((lang, index) => {
|
items: langs.map((lang) => {
|
||||||
return createProgressTextNode({
|
return createProgressTextNode({
|
||||||
width,
|
width: width,
|
||||||
name: lang.name,
|
name: lang.name,
|
||||||
color: lang.color || DEFAULT_LANG_COLOR,
|
color: lang.color || DEFAULT_LANG_COLOR,
|
||||||
progress: ((lang.size / totalLanguageSize) * 100).toFixed(2),
|
progress: ((lang.size / totalLanguageSize) * 100).toFixed(2),
|
||||||
index,
|
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
gap: 40,
|
gap: 40,
|
||||||
@@ -163,10 +153,9 @@ const renderNormalLayout = (langs, width, totalLanguageSize) => {
|
|||||||
* @param {Lang[]} langs Array of programming languages.
|
* @param {Lang[]} langs Array of programming languages.
|
||||||
* @param {number} width Card width.
|
* @param {number} width Card width.
|
||||||
* @param {number} totalLanguageSize Total size of all languages.
|
* @param {number} totalLanguageSize Total size of all languages.
|
||||||
* @param {boolean} hideProgress Whether to hide progress bar.
|
|
||||||
* @returns {string} Compact layout card SVG object.
|
* @returns {string} Compact layout card SVG object.
|
||||||
*/
|
*/
|
||||||
const renderCompactLayout = (langs, width, totalLanguageSize, hideProgress) => {
|
const renderCompactLayout = (langs, width, totalLanguageSize) => {
|
||||||
const paddingRight = 50;
|
const paddingRight = 50;
|
||||||
const offsetWidth = width - paddingRight;
|
const offsetWidth = width - paddingRight;
|
||||||
// progressOffset holds the previous language's width and used to offset the next language
|
// progressOffset holds the previous language's width and used to offset the next language
|
||||||
@@ -197,21 +186,15 @@ const renderCompactLayout = (langs, width, totalLanguageSize, hideProgress) => {
|
|||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
return `
|
return `
|
||||||
${
|
<mask id="rect-mask">
|
||||||
!hideProgress
|
<rect x="0" y="0" width="${offsetWidth}" height="8" fill="white" rx="5" />
|
||||||
? `
|
|
||||||
<mask id="rect-mask">
|
|
||||||
<rect x="0" y="0" width="${offsetWidth}" height="8" fill="white" rx="5"/>
|
|
||||||
</mask>
|
</mask>
|
||||||
${compactProgressBar}
|
${compactProgressBar}
|
||||||
`
|
|
||||||
: ""
|
<g transform="translate(0, 25)">
|
||||||
}
|
|
||||||
<g transform="translate(0, ${hideProgress ? "0" : "25"})">
|
|
||||||
${createLanguageTextNode({
|
${createLanguageTextNode({
|
||||||
langs,
|
langs,
|
||||||
totalSize: totalLanguageSize,
|
totalSize: totalLanguageSize,
|
||||||
hideProgress: hideProgress,
|
|
||||||
})}
|
})}
|
||||||
</g>
|
</g>
|
||||||
`;
|
`;
|
||||||
@@ -286,7 +269,6 @@ const renderTopLanguages = (topLangs, options = {}) => {
|
|||||||
text_color,
|
text_color,
|
||||||
bg_color,
|
bg_color,
|
||||||
hide,
|
hide,
|
||||||
hide_progress,
|
|
||||||
theme,
|
theme,
|
||||||
layout,
|
layout,
|
||||||
custom_title,
|
custom_title,
|
||||||
@@ -294,7 +276,6 @@ const renderTopLanguages = (topLangs, options = {}) => {
|
|||||||
langs_count = DEFAULT_LANGS_COUNT,
|
langs_count = DEFAULT_LANGS_COUNT,
|
||||||
border_radius,
|
border_radius,
|
||||||
border_color,
|
border_color,
|
||||||
disable_animations,
|
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const i18n = new I18n({
|
const i18n = new I18n({
|
||||||
@@ -316,17 +297,11 @@ const renderTopLanguages = (topLangs, options = {}) => {
|
|||||||
let height = calculateNormalLayoutHeight(langs.length);
|
let height = calculateNormalLayoutHeight(langs.length);
|
||||||
|
|
||||||
let finalLayout = "";
|
let finalLayout = "";
|
||||||
if (layout === "compact" || hide_progress == true) {
|
if (layout === "compact") {
|
||||||
width = width + 50; // padding
|
width = width + 50; // padding
|
||||||
height =
|
height = calculateCompactLayoutHeight(langs.length);
|
||||||
calculateCompactLayoutHeight(langs.length) + (hide_progress ? -25 : 0);
|
|
||||||
|
|
||||||
finalLayout = renderCompactLayout(
|
finalLayout = renderCompactLayout(langs, width, totalLanguageSize);
|
||||||
langs,
|
|
||||||
width,
|
|
||||||
totalLanguageSize,
|
|
||||||
hide_progress,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
finalLayout = renderNormalLayout(langs, width, totalLanguageSize);
|
finalLayout = renderNormalLayout(langs, width, totalLanguageSize);
|
||||||
}
|
}
|
||||||
@@ -349,43 +324,11 @@ const renderTopLanguages = (topLangs, options = {}) => {
|
|||||||
colors,
|
colors,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (disable_animations) card.disableAnimations();
|
card.disableAnimations();
|
||||||
|
|
||||||
card.setHideBorder(hide_border);
|
card.setHideBorder(hide_border);
|
||||||
card.setHideTitle(hide_title);
|
card.setHideTitle(hide_title);
|
||||||
card.setCSS(
|
card.setCSS(
|
||||||
`
|
`.lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.textColor} }`,
|
||||||
@keyframes slideInAnimation {
|
|
||||||
from {
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
width: calc(100%-100px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes growWidthAnimation {
|
|
||||||
from {
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.lang-name {
|
|
||||||
font: 400 11px "Segoe UI", Ubuntu, Sans-Serif;
|
|
||||||
fill: ${colors.textColor};
|
|
||||||
}
|
|
||||||
.stagger {
|
|
||||||
opacity: 0;
|
|
||||||
animation: fadeInAnimation 0.3s ease-in-out forwards;
|
|
||||||
}
|
|
||||||
#rect-mask rect{
|
|
||||||
animation: slideInAnimation 1s ease-in-out forwards;
|
|
||||||
}
|
|
||||||
.lang-progress{
|
|
||||||
animation: growWidthAnimation 0.6s ease-in-out forwards;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return card.render(`
|
return card.render(`
|
||||||
|
|||||||
Vendored
-2
@@ -37,8 +37,6 @@ export type TopLangOptions = CommonOptions & {
|
|||||||
layout: "compact" | "normal";
|
layout: "compact" | "normal";
|
||||||
custom_title: string;
|
custom_title: string;
|
||||||
langs_count: number;
|
langs_count: number;
|
||||||
disable_animations: boolean;
|
|
||||||
hide_progress: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type WakaTimeOptions = CommonOptions & {
|
type WakaTimeOptions = CommonOptions & {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
getCardColors,
|
getCardColors,
|
||||||
lowercaseTrim,
|
lowercaseTrim,
|
||||||
} from "../common/utils.js";
|
} from "../common/utils.js";
|
||||||
import { getStyles } from "../getStyles.js";
|
import { getStyles } from "../common/getStyles.js";
|
||||||
import { wakatimeCardLocales } from "../translations.js";
|
import { wakatimeCardLocales } from "../translations.js";
|
||||||
|
|
||||||
/** Import language colors.
|
/** Import language colors.
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { getAnimations } from "../getStyles.js";
|
import { getAnimations } from "./getStyles.js";
|
||||||
import { encodeHTML, flexLayout } from "./utils.js";
|
import { encodeHTML, flexLayout } from "./utils.js";
|
||||||
|
|
||||||
class Card {
|
class Card {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { clampValue } from "./utils.js";
|
|||||||
* @param {string} createProgressNodeParams.color Progress color.
|
* @param {string} createProgressNodeParams.color Progress color.
|
||||||
* @param {string} createProgressNodeParams.progress Progress value.
|
* @param {string} createProgressNodeParams.progress Progress value.
|
||||||
* @param {string} createProgressNodeParams.progressBarBackgroundColor Progress bar bg color.
|
* @param {string} createProgressNodeParams.progressBarBackgroundColor Progress bar bg color.
|
||||||
* @param {number} createProgressNodeParams.delay Delay before animation starts.
|
|
||||||
* @returns {string} Progress node.
|
* @returns {string} Progress node.
|
||||||
*/
|
*/
|
||||||
const createProgressNode = ({
|
const createProgressNode = ({
|
||||||
@@ -20,22 +19,20 @@ const createProgressNode = ({
|
|||||||
color,
|
color,
|
||||||
progress,
|
progress,
|
||||||
progressBarBackgroundColor,
|
progressBarBackgroundColor,
|
||||||
delay,
|
|
||||||
}) => {
|
}) => {
|
||||||
const progressPercentage = clampValue(progress, 2, 100);
|
const progressPercentage = clampValue(progress, 2, 100);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<svg width="${width}" x="${x}" y="${y}">
|
<svg width="${width}" x="${x}" y="${y}">
|
||||||
<rect rx="5" ry="5" x="0" y="0" width="${width}" height="8" fill="${progressBarBackgroundColor}"></rect>
|
<rect rx="5" ry="5" x="0" y="0" width="${width}" height="8" fill="${progressBarBackgroundColor}"></rect>
|
||||||
<svg data-testid="lang-progress" width="${progressPercentage}%">
|
<rect
|
||||||
<rect
|
height="8"
|
||||||
height="8"
|
fill="${color}"
|
||||||
fill="${color}"
|
rx="5" ry="5" x="0" y="0"
|
||||||
rx="5" ry="5" x="0" y="0"
|
data-testid="lang-progress"
|
||||||
class="lang-progress"
|
width="${progressPercentage}%"
|
||||||
style="animation-delay: ${delay}ms;"
|
>
|
||||||
/>
|
</rect>
|
||||||
</svg>
|
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -102,7 +102,6 @@
|
|||||||
"Csound Score": "#1a1a1a",
|
"Csound Score": "#1a1a1a",
|
||||||
"Cuda": "#3A4E3A",
|
"Cuda": "#3A4E3A",
|
||||||
"Curry": "#531242",
|
"Curry": "#531242",
|
||||||
"Cypher": "#34c0eb",
|
|
||||||
"Cython": "#fedf5b",
|
"Cython": "#fedf5b",
|
||||||
"D": "#ba595e",
|
"D": "#ba595e",
|
||||||
"DM": "#447265",
|
"DM": "#447265",
|
||||||
@@ -125,7 +124,6 @@
|
|||||||
"Earthly": "#2af0ff",
|
"Earthly": "#2af0ff",
|
||||||
"Easybuild": "#069406",
|
"Easybuild": "#069406",
|
||||||
"Ecere Projects": "#913960",
|
"Ecere Projects": "#913960",
|
||||||
"Ecmarkup": "#eb8131",
|
|
||||||
"EditorConfig": "#fff1f2",
|
"EditorConfig": "#fff1f2",
|
||||||
"Eiffel": "#4d6977",
|
"Eiffel": "#4d6977",
|
||||||
"Elixir": "#6e4a7e",
|
"Elixir": "#6e4a7e",
|
||||||
@@ -217,7 +215,6 @@
|
|||||||
"Idris": "#b30000",
|
"Idris": "#b30000",
|
||||||
"Ignore List": "#000000",
|
"Ignore List": "#000000",
|
||||||
"ImageJ Macro": "#99AAFF",
|
"ImageJ Macro": "#99AAFF",
|
||||||
"Imba": "#16cec6",
|
|
||||||
"Inno Setup": "#264b99",
|
"Inno Setup": "#264b99",
|
||||||
"Io": "#a9188d",
|
"Io": "#a9188d",
|
||||||
"Ioke": "#078193",
|
"Ioke": "#078193",
|
||||||
@@ -289,7 +286,6 @@
|
|||||||
"Mathematica": "#dd1100",
|
"Mathematica": "#dd1100",
|
||||||
"Max": "#c4a79c",
|
"Max": "#c4a79c",
|
||||||
"Mercury": "#ff2b2b",
|
"Mercury": "#ff2b2b",
|
||||||
"Mermaid": "#ff3670",
|
|
||||||
"Meson": "#007800",
|
"Meson": "#007800",
|
||||||
"Metal": "#8f14e9",
|
"Metal": "#8f14e9",
|
||||||
"MiniYAML": "#ff1111",
|
"MiniYAML": "#ff1111",
|
||||||
@@ -322,10 +318,6 @@
|
|||||||
"Nu": "#c9df40",
|
"Nu": "#c9df40",
|
||||||
"NumPy": "#9C8AF9",
|
"NumPy": "#9C8AF9",
|
||||||
"Nunjucks": "#3d8137",
|
"Nunjucks": "#3d8137",
|
||||||
"OASv2-json": "#85ea2d",
|
|
||||||
"OASv2-yaml": "#85ea2d",
|
|
||||||
"OASv3-json": "#85ea2d",
|
|
||||||
"OASv3-yaml": "#85ea2d",
|
|
||||||
"OCaml": "#3be133",
|
"OCaml": "#3be133",
|
||||||
"ObjectScript": "#424893",
|
"ObjectScript": "#424893",
|
||||||
"Objective-C": "#438eff",
|
"Objective-C": "#438eff",
|
||||||
@@ -335,18 +327,14 @@
|
|||||||
"Omgrofl": "#cabbff",
|
"Omgrofl": "#cabbff",
|
||||||
"Opal": "#f7ede0",
|
"Opal": "#f7ede0",
|
||||||
"Open Policy Agent": "#7d9199",
|
"Open Policy Agent": "#7d9199",
|
||||||
"OpenAPI Specification v2": "#85ea2d",
|
|
||||||
"OpenAPI Specification v3": "#85ea2d",
|
|
||||||
"OpenCL": "#ed2e2d",
|
"OpenCL": "#ed2e2d",
|
||||||
"OpenEdge ABL": "#5ce600",
|
"OpenEdge ABL": "#5ce600",
|
||||||
"OpenQASM": "#AA70FF",
|
"OpenQASM": "#AA70FF",
|
||||||
"OpenSCAD": "#e5cd45",
|
"OpenSCAD": "#e5cd45",
|
||||||
"Option List": "#476732",
|
|
||||||
"Org": "#77aa99",
|
"Org": "#77aa99",
|
||||||
"Oxygene": "#cdd0e3",
|
"Oxygene": "#cdd0e3",
|
||||||
"Oz": "#fab738",
|
"Oz": "#fab738",
|
||||||
"P4": "#7055b5",
|
"P4": "#7055b5",
|
||||||
"PDDL": "#0d00ff",
|
|
||||||
"PEG.js": "#234d6b",
|
"PEG.js": "#234d6b",
|
||||||
"PHP": "#4F5D95",
|
"PHP": "#4F5D95",
|
||||||
"PLSQL": "#dad8d8",
|
"PLSQL": "#dad8d8",
|
||||||
@@ -363,7 +351,6 @@
|
|||||||
"PigLatin": "#fcd7de",
|
"PigLatin": "#fcd7de",
|
||||||
"Pike": "#005390",
|
"Pike": "#005390",
|
||||||
"PogoScript": "#d80074",
|
"PogoScript": "#d80074",
|
||||||
"Polar": "#ae81ff",
|
|
||||||
"Portugol": "#f8bd00",
|
"Portugol": "#f8bd00",
|
||||||
"PostCSS": "#dc3a0c",
|
"PostCSS": "#dc3a0c",
|
||||||
"PostScript": "#da291c",
|
"PostScript": "#da291c",
|
||||||
@@ -427,7 +414,6 @@
|
|||||||
"Sass": "#a53b70",
|
"Sass": "#a53b70",
|
||||||
"Scala": "#c22d40",
|
"Scala": "#c22d40",
|
||||||
"Scaml": "#bd181a",
|
"Scaml": "#bd181a",
|
||||||
"Scenic": "#fdc700",
|
|
||||||
"Scheme": "#1e4aec",
|
"Scheme": "#1e4aec",
|
||||||
"Scilab": "#ca0f21",
|
"Scilab": "#ca0f21",
|
||||||
"Self": "#0579aa",
|
"Self": "#0579aa",
|
||||||
@@ -435,7 +421,6 @@
|
|||||||
"Shell": "#89e051",
|
"Shell": "#89e051",
|
||||||
"ShellCheck Config": "#cecfcb",
|
"ShellCheck Config": "#cecfcb",
|
||||||
"Shen": "#120F14",
|
"Shen": "#120F14",
|
||||||
"Simple File Verification": "#C9BFED",
|
|
||||||
"Singularity": "#64E6AD",
|
"Singularity": "#64E6AD",
|
||||||
"Slash": "#007eff",
|
"Slash": "#007eff",
|
||||||
"Slice": "#003fa2",
|
"Slice": "#003fa2",
|
||||||
@@ -443,7 +428,6 @@
|
|||||||
"SmPL": "#c94949",
|
"SmPL": "#c94949",
|
||||||
"Smalltalk": "#596706",
|
"Smalltalk": "#596706",
|
||||||
"Smarty": "#f0c040",
|
"Smarty": "#f0c040",
|
||||||
"Smithy": "#c44536",
|
|
||||||
"Solidity": "#AA6746",
|
"Solidity": "#AA6746",
|
||||||
"SourcePawn": "#f69e1d",
|
"SourcePawn": "#f69e1d",
|
||||||
"Squirrel": "#800000",
|
"Squirrel": "#800000",
|
||||||
@@ -494,7 +478,6 @@
|
|||||||
"Vim Script": "#199f4b",
|
"Vim Script": "#199f4b",
|
||||||
"Vim Snippet": "#199f4b",
|
"Vim Snippet": "#199f4b",
|
||||||
"Visual Basic .NET": "#945db7",
|
"Visual Basic .NET": "#945db7",
|
||||||
"Visual Basic 6.0": "#2c6353",
|
|
||||||
"Volt": "#1F1F1F",
|
"Volt": "#1F1F1F",
|
||||||
"Vue": "#41b883",
|
"Vue": "#41b883",
|
||||||
"Vyper": "#2980b9",
|
"Vyper": "#2980b9",
|
||||||
@@ -531,7 +514,6 @@
|
|||||||
"fish": "#4aae47",
|
"fish": "#4aae47",
|
||||||
"hoon": "#00b171",
|
"hoon": "#00b171",
|
||||||
"jq": "#c7254e",
|
"jq": "#c7254e",
|
||||||
"just": "#384d54",
|
|
||||||
"kvlang": "#1da6e0",
|
"kvlang": "#1da6e0",
|
||||||
"mIRC Script": "#3d57c3",
|
"mIRC Script": "#3d57c3",
|
||||||
"mcfunction": "#E22837",
|
"mcfunction": "#E22837",
|
||||||
|
|||||||
@@ -424,19 +424,6 @@ const parseEmojis = (str) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Get diff in minutes
|
|
||||||
* @param {Date} d1
|
|
||||||
* @param {Date} d2
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
const dateDiff = (d1, d2) => {
|
|
||||||
const date1 = new Date(d1);
|
|
||||||
const date2 = new Date(d2);
|
|
||||||
const diff = date1.getTime() - date2.getTime();
|
|
||||||
return Math.round(diff / (1000 * 60));
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ERROR_CARD_LENGTH,
|
ERROR_CARD_LENGTH,
|
||||||
renderError,
|
renderError,
|
||||||
@@ -460,5 +447,4 @@ export {
|
|||||||
lowercaseTrim,
|
lowercaseTrim,
|
||||||
chunkArray,
|
chunkArray,
|
||||||
parseEmojis,
|
parseEmojis,
|
||||||
dateDiff,
|
|
||||||
};
|
};
|
||||||
|
|||||||
+105
-104
@@ -1,8 +1,7 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import * as dotenv from "dotenv";
|
|
||||||
import githubUsernameRegex from "github-username-regex";
|
import githubUsernameRegex from "github-username-regex";
|
||||||
import { calculateRank } from "../calculateRank.js";
|
import { calculateRank } from "../common/calculateRank.js";
|
||||||
import { retryer } from "../common/retryer.js";
|
import { retryer } from "../common/retryer.js";
|
||||||
import {
|
import {
|
||||||
CustomError,
|
CustomError,
|
||||||
@@ -12,74 +11,46 @@ import {
|
|||||||
wrapTextMultiline,
|
wrapTextMultiline,
|
||||||
} from "../common/utils.js";
|
} from "../common/utils.js";
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
// GraphQL queries.
|
|
||||||
const GRAPHQL_REPOS_FIELD = `
|
|
||||||
repositories(first: 100, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}, after: $after) {
|
|
||||||
totalCount
|
|
||||||
nodes {
|
|
||||||
name
|
|
||||||
stargazers {
|
|
||||||
totalCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pageInfo {
|
|
||||||
hasNextPage
|
|
||||||
endCursor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const GRAPHQL_REPOS_QUERY = `
|
|
||||||
query userInfo($login: String!, $after: String) {
|
|
||||||
user(login: $login) {
|
|
||||||
${GRAPHQL_REPOS_FIELD}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const GRAPHQL_STATS_QUERY = `
|
|
||||||
query userInfo($login: String!, $after: String) {
|
|
||||||
user(login: $login) {
|
|
||||||
name
|
|
||||||
login
|
|
||||||
contributionsCollection {
|
|
||||||
totalCommitContributions
|
|
||||||
restrictedContributionsCount
|
|
||||||
}
|
|
||||||
repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
|
|
||||||
totalCount
|
|
||||||
}
|
|
||||||
pullRequests(first: 1) {
|
|
||||||
totalCount
|
|
||||||
}
|
|
||||||
openIssues: issues(states: OPEN) {
|
|
||||||
totalCount
|
|
||||||
}
|
|
||||||
closedIssues: issues(states: CLOSED) {
|
|
||||||
totalCount
|
|
||||||
}
|
|
||||||
followers {
|
|
||||||
totalCount
|
|
||||||
}
|
|
||||||
${GRAPHQL_REPOS_FIELD}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stats fetcher object.
|
* Stats fetcher object.
|
||||||
*
|
*
|
||||||
* @param {import('axios').AxiosRequestHeaders} variables Fetcher variables.
|
* @param {import('axios').AxiosRequestHeaders} variables Fetcher variables.
|
||||||
* @param {string} token GitHub token.
|
* @param {string} token GitHub token.
|
||||||
* @returns {Promise<import('../common/types').Fetcher>} Stats fetcher response.
|
* @returns {Promise<import('../common/types').StatsFetcherResponse>} Stats fetcher response.
|
||||||
*/
|
*/
|
||||||
const fetcher = (variables, token) => {
|
const fetcher = (variables, token) => {
|
||||||
const query = !variables.after ? GRAPHQL_STATS_QUERY : GRAPHQL_REPOS_QUERY;
|
|
||||||
return request(
|
return request(
|
||||||
{
|
{
|
||||||
query,
|
query: `
|
||||||
|
query userInfo($login: String!) {
|
||||||
|
user(login: $login) {
|
||||||
|
name
|
||||||
|
login
|
||||||
|
contributionsCollection {
|
||||||
|
totalCommitContributions
|
||||||
|
restrictedContributionsCount
|
||||||
|
}
|
||||||
|
repositoriesContributedTo(contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
pullRequests {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
openIssues: issues(states: OPEN) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
closedIssues: issues(states: CLOSED) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
followers {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
repositories(ownerAffiliations: OWNER) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
variables,
|
variables,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -89,42 +60,39 @@ const fetcher = (variables, token) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch stats information for a given username.
|
* Fetch first 100 repositories for a given username.
|
||||||
*
|
*
|
||||||
* @param {string} username Github username.
|
* @param {import('axios').AxiosRequestHeaders} variables Fetcher variables.
|
||||||
* @returns {Promise<import('../common/types').StatsFetcher>} GraphQL Stats object.
|
* @param {string} token GitHub token.
|
||||||
*
|
* @returns {Promise<import('../common/types').StatsFetcherResponse>} Repositories fetcher response.
|
||||||
* @description This function supports multi-page fetching if the 'FETCH_MULTI_PAGE_STARS' environment variable is set to true.
|
|
||||||
*/
|
*/
|
||||||
const statsFetcher = async (username) => {
|
const repositoriesFetcher = (variables, token) => {
|
||||||
let stats;
|
return request(
|
||||||
let hasNextPage = true;
|
{
|
||||||
let endCursor = null;
|
query: `
|
||||||
while (hasNextPage) {
|
query userInfo($login: String!, $after: String) {
|
||||||
const variables = { login: username, first: 100, after: endCursor };
|
user(login: $login) {
|
||||||
let res = await retryer(fetcher, variables);
|
repositories(first: 100, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}, after: $after) {
|
||||||
if (res.data.errors) return res;
|
nodes {
|
||||||
|
name
|
||||||
// Store stats data.
|
stargazers {
|
||||||
const repoNodes = res.data.data.user.repositories.nodes;
|
totalCount
|
||||||
if (!stats) {
|
}
|
||||||
stats = res;
|
}
|
||||||
} else {
|
pageInfo {
|
||||||
stats.data.data.user.repositories.nodes.push(...repoNodes);
|
hasNextPage
|
||||||
}
|
endCursor
|
||||||
|
}
|
||||||
// Disable multi page fetching on public Vercel instance due to rate limits.
|
}
|
||||||
const repoNodesWithStars = repoNodes.filter(
|
}
|
||||||
(node) => node.stargazers.totalCount !== 0,
|
}
|
||||||
);
|
`,
|
||||||
hasNextPage =
|
variables,
|
||||||
process.env.FETCH_MULTI_PAGE_STARS === "true" &&
|
},
|
||||||
repoNodes.length === repoNodesWithStars.length &&
|
{
|
||||||
res.data.data.user.repositories.pageInfo.hasNextPage;
|
Authorization: `bearer ${token}`,
|
||||||
endCursor = res.data.data.user.repositories.pageInfo.endCursor;
|
},
|
||||||
}
|
);
|
||||||
|
|
||||||
return stats;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -169,6 +137,46 @@ const totalCommitsFetcher = async (username) => {
|
|||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all the stars for all the repositories of a given username.
|
||||||
|
*
|
||||||
|
* @param {string} username GitHub username.
|
||||||
|
* @param {array} repoToHide Repositories to hide.
|
||||||
|
* @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.
|
* Fetch stats for a given username.
|
||||||
*
|
*
|
||||||
@@ -195,7 +203,7 @@ const fetchStats = async (
|
|||||||
rank: { level: "C", score: 0 },
|
rank: { level: "C", score: 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
let res = await statsFetcher(username);
|
let res = await retryer(fetcher, { login: username });
|
||||||
|
|
||||||
// Catch GraphQL errors.
|
// Catch GraphQL errors.
|
||||||
if (res.data.errors) {
|
if (res.data.errors) {
|
||||||
@@ -251,15 +259,8 @@ const fetchStats = async (
|
|||||||
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 = user.repositories.nodes
|
stats.totalStars = await totalStarsFetcher(username, repoToHide);
|
||||||
.filter((data) => {
|
|
||||||
return !repoToHide[data.name];
|
|
||||||
})
|
|
||||||
.reduce((prev, curr) => {
|
|
||||||
return prev + curr.stargazers.totalCount;
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
// @ts-ignore // TODO: Fix this.
|
|
||||||
stats.rank = calculateRank({
|
stats.rank = calculateRank({
|
||||||
totalCommits: stats.totalCommits,
|
totalCommits: stats.totalCommits,
|
||||||
totalRepos: user.repositories.totalCount,
|
totalRepos: user.repositories.totalCount,
|
||||||
|
|||||||
+25
-12
@@ -2,7 +2,7 @@ import { jest } from "@jest/globals";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import MockAdapter from "axios-mock-adapter";
|
import MockAdapter from "axios-mock-adapter";
|
||||||
import api from "../api/index.js";
|
import api from "../api/index.js";
|
||||||
import { calculateRank } from "../src/calculateRank.js";
|
import { calculateRank } from "../src/common/calculateRank.js";
|
||||||
import { renderStatsCard } from "../src/cards/stats-card.js";
|
import { renderStatsCard } from "../src/cards/stats-card.js";
|
||||||
import { CONSTANTS, renderError } from "../src/common/utils.js";
|
import { CONSTANTS, renderError } from "../src/common/utils.js";
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ stats.rank = calculateRank({
|
|||||||
issues: stats.totalIssues,
|
issues: stats.totalIssues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const data_stats = {
|
const data = {
|
||||||
data: {
|
data: {
|
||||||
user: {
|
user: {
|
||||||
name: stats.name,
|
name: stats.name,
|
||||||
@@ -40,6 +40,15 @@ const data_stats = {
|
|||||||
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: {
|
pageInfo: {
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
@@ -74,7 +83,11 @@ const faker = (query, data) => {
|
|||||||
setHeader: jest.fn(),
|
setHeader: jest.fn(),
|
||||||
send: jest.fn(),
|
send: jest.fn(),
|
||||||
};
|
};
|
||||||
mock.onPost("https://api.github.com/graphql").replyOnce(200, data);
|
mock
|
||||||
|
.onPost("https://api.github.com/graphql")
|
||||||
|
.replyOnce(200, data)
|
||||||
|
.onPost("https://api.github.com/graphql")
|
||||||
|
.replyOnce(200, repositoriesData);
|
||||||
|
|
||||||
return { req, res };
|
return { req, res };
|
||||||
};
|
};
|
||||||
@@ -85,7 +98,7 @@ afterEach(() => {
|
|||||||
|
|
||||||
describe("Test /api/", () => {
|
describe("Test /api/", () => {
|
||||||
it("should test the request", async () => {
|
it("should test the request", async () => {
|
||||||
const { req, res } = faker({}, data_stats);
|
const { req, res } = faker({}, data);
|
||||||
|
|
||||||
await api(req, res);
|
await api(req, res);
|
||||||
|
|
||||||
@@ -120,7 +133,7 @@ describe("Test /api/", () => {
|
|||||||
text_color: "fff",
|
text_color: "fff",
|
||||||
bg_color: "fff",
|
bg_color: "fff",
|
||||||
},
|
},
|
||||||
data_stats,
|
data,
|
||||||
);
|
);
|
||||||
|
|
||||||
await api(req, res);
|
await api(req, res);
|
||||||
@@ -141,7 +154,7 @@ describe("Test /api/", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should have proper cache", async () => {
|
it("should have proper cache", async () => {
|
||||||
const { req, res } = faker({}, data_stats);
|
const { req, res } = faker({}, data);
|
||||||
|
|
||||||
await api(req, res);
|
await api(req, res);
|
||||||
|
|
||||||
@@ -157,7 +170,7 @@ describe("Test /api/", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should set proper cache", async () => {
|
it("should set proper cache", async () => {
|
||||||
const { req, res } = faker({ cache_seconds: 15000 }, data_stats);
|
const { req, res } = faker({ cache_seconds: 15000 }, data);
|
||||||
await api(req, res);
|
await api(req, res);
|
||||||
|
|
||||||
expect(res.setHeader.mock.calls).toEqual([
|
expect(res.setHeader.mock.calls).toEqual([
|
||||||
@@ -183,7 +196,7 @@ describe("Test /api/", () => {
|
|||||||
|
|
||||||
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_stats);
|
let { req, res } = faker({ cache_seconds: 200000 }, data);
|
||||||
await api(req, res);
|
await api(req, res);
|
||||||
|
|
||||||
expect(res.setHeader.mock.calls).toEqual([
|
expect(res.setHeader.mock.calls).toEqual([
|
||||||
@@ -199,7 +212,7 @@ describe("Test /api/", () => {
|
|||||||
|
|
||||||
// note i'm using block scoped vars
|
// note i'm using block scoped vars
|
||||||
{
|
{
|
||||||
let { req, res } = faker({ cache_seconds: 0 }, data_stats);
|
let { req, res } = faker({ cache_seconds: 0 }, data);
|
||||||
await api(req, res);
|
await api(req, res);
|
||||||
|
|
||||||
expect(res.setHeader.mock.calls).toEqual([
|
expect(res.setHeader.mock.calls).toEqual([
|
||||||
@@ -214,7 +227,7 @@ describe("Test /api/", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let { req, res } = faker({ cache_seconds: -10000 }, data_stats);
|
let { req, res } = faker({ cache_seconds: -10000 }, data);
|
||||||
await api(req, res);
|
await api(req, res);
|
||||||
|
|
||||||
expect(res.setHeader.mock.calls).toEqual([
|
expect(res.setHeader.mock.calls).toEqual([
|
||||||
@@ -235,7 +248,7 @@ describe("Test /api/", () => {
|
|||||||
username: "anuraghazra",
|
username: "anuraghazra",
|
||||||
count_private: true,
|
count_private: true,
|
||||||
},
|
},
|
||||||
data_stats,
|
data,
|
||||||
);
|
);
|
||||||
|
|
||||||
await api(req, res);
|
await api(req, res);
|
||||||
@@ -275,7 +288,7 @@ describe("Test /api/", () => {
|
|||||||
text_color: "fff",
|
text_color: "fff",
|
||||||
bg_color: "fff",
|
bg_color: "fff",
|
||||||
},
|
},
|
||||||
data_stats,
|
data,
|
||||||
);
|
);
|
||||||
|
|
||||||
await api(req, res);
|
await api(req, res);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import { calculateRank } from "../src/calculateRank.js";
|
import { calculateRank } from "../src/common/calculateRank.js";
|
||||||
|
|
||||||
describe("Test calculateRank", () => {
|
describe("Test calculateRank", () => {
|
||||||
it("should calculate rank correctly", () => {
|
it("should calculate rank correctly", () => {
|
||||||
|
|||||||
+25
-20
@@ -11,22 +11,27 @@ 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";
|
||||||
|
|
||||||
const REPO = "curly-fiesta";
|
const REPO = "dummy-cra";
|
||||||
const USER = "catelinemnemosyne";
|
const USER = "grsdummy";
|
||||||
const STATS_DATA = {
|
const STATS_DATA = {
|
||||||
name: "Cateline Mnemosyne",
|
name: "grsdummy",
|
||||||
totalPRs: 1,
|
totalPRs: 2,
|
||||||
totalCommits: 7,
|
totalCommits: 2,
|
||||||
totalIssues: 1,
|
totalIssues: 1,
|
||||||
totalStars: 1,
|
totalStars: 1,
|
||||||
contributedTo: 1,
|
contributedTo: 2,
|
||||||
rank: {
|
rank: {
|
||||||
level: "A+",
|
level: "A+",
|
||||||
score: 50.893750297869225,
|
score: 50.900829325065935,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const LANGS_DATA = {
|
const LANGS_DATA = {
|
||||||
|
TypeScript: {
|
||||||
|
color: "#3178c6",
|
||||||
|
name: "TypeScript",
|
||||||
|
size: 2049,
|
||||||
|
},
|
||||||
HTML: {
|
HTML: {
|
||||||
color: "#e34c26",
|
color: "#e34c26",
|
||||||
name: "HTML",
|
name: "HTML",
|
||||||
@@ -37,19 +42,19 @@ const LANGS_DATA = {
|
|||||||
name: "CSS",
|
name: "CSS",
|
||||||
size: 930,
|
size: 930,
|
||||||
},
|
},
|
||||||
JavaScript: {
|
Python: {
|
||||||
color: "#f1e05a",
|
color: "#3572A5",
|
||||||
name: "JavaScript",
|
name: "Python",
|
||||||
size: 1912,
|
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,
|
||||||
is_coding_activity_visible: true,
|
is_coding_activity_visible: false,
|
||||||
is_including_today: false,
|
is_including_today: false,
|
||||||
is_other_usage_visible: true,
|
is_other_usage_visible: false,
|
||||||
is_stuck: false,
|
is_stuck: false,
|
||||||
is_up_to_date: false,
|
is_up_to_date: false,
|
||||||
is_up_to_date_pending_future: false,
|
is_up_to_date_pending_future: false,
|
||||||
@@ -57,24 +62,24 @@ const WAKATIME_DATA = {
|
|||||||
range: "last_7_days",
|
range: "last_7_days",
|
||||||
status: "pending_update",
|
status: "pending_update",
|
||||||
timeout: 15,
|
timeout: 15,
|
||||||
username: USER,
|
username: "grsdummy",
|
||||||
writes_only: false,
|
writes_only: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const REPOSITORY_DATA = {
|
const REPOSITORY_DATA = {
|
||||||
name: REPO,
|
name: "dummy-cra",
|
||||||
nameWithOwner: `${USER}/cra-test`,
|
nameWithOwner: "grsdummy/dummy-cra",
|
||||||
isPrivate: false,
|
isPrivate: false,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
isTemplate: false,
|
isTemplate: false,
|
||||||
stargazers: {
|
stargazers: {
|
||||||
totalCount: 1,
|
totalCount: 1,
|
||||||
},
|
},
|
||||||
description: "Simple cra test repo.",
|
description: "Dummy create react app.",
|
||||||
primaryLanguage: {
|
primaryLanguage: {
|
||||||
color: "#f1e05a",
|
color: "#3178c6",
|
||||||
id: "MDg6TGFuZ3VhZ2UxNDA=",
|
id: "MDg6TGFuZ3VhZ2UyODc=",
|
||||||
name: "JavaScript",
|
name: "TypeScript",
|
||||||
},
|
},
|
||||||
forkCount: 0,
|
forkCount: 0,
|
||||||
starCount: 1,
|
starCount: 1,
|
||||||
|
|||||||
+35
-93
@@ -1,11 +1,10 @@
|
|||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import MockAdapter from "axios-mock-adapter";
|
import MockAdapter from "axios-mock-adapter";
|
||||||
import { calculateRank } from "../src/calculateRank.js";
|
import { calculateRank } from "../src/common/calculateRank.js";
|
||||||
import { fetchStats } from "../src/fetchers/stats-fetcher.js";
|
import { fetchStats } from "../src/fetchers/stats-fetcher.js";
|
||||||
|
|
||||||
// Test parameters.
|
const data = {
|
||||||
const data_stats = {
|
|
||||||
data: {
|
data: {
|
||||||
user: {
|
user: {
|
||||||
name: "Anurag Hazra",
|
name: "Anurag Hazra",
|
||||||
@@ -20,6 +19,15 @@ const data_stats = {
|
|||||||
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 } },
|
||||||
@@ -34,7 +42,7 @@ const data_stats = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const data_repo = {
|
const secondRepositoriesData = {
|
||||||
data: {
|
data: {
|
||||||
user: {
|
user: {
|
||||||
repositories: {
|
repositories: {
|
||||||
@@ -51,7 +59,7 @@ const data_repo = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const data_repo_zero_stars = {
|
const repositoriesWithZeroStarsData = {
|
||||||
data: {
|
data: {
|
||||||
user: {
|
user: {
|
||||||
repositories: {
|
repositories: {
|
||||||
@@ -85,12 +93,13 @@ const error = {
|
|||||||
const mock = new MockAdapter(axios);
|
const mock = new MockAdapter(axios);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
process.env.FETCH_MULTI_PAGE_STARS = "false"; // Set to `false` to fetch only one page of stars.
|
|
||||||
mock
|
mock
|
||||||
.onPost("https://api.github.com/graphql")
|
.onPost("https://api.github.com/graphql")
|
||||||
.replyOnce(200, data_stats)
|
.replyOnce(200, data)
|
||||||
.onPost("https://api.github.com/graphql")
|
.onPost("https://api.github.com/graphql")
|
||||||
.replyOnce(200, data_repo);
|
.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(() => {
|
||||||
@@ -105,7 +114,8 @@ describe("Test fetchStats", () => {
|
|||||||
totalRepos: 5,
|
totalRepos: 5,
|
||||||
followers: 100,
|
followers: 100,
|
||||||
contributions: 61,
|
contributions: 61,
|
||||||
stargazers: 300,
|
// 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,
|
prs: 300,
|
||||||
issues: 200,
|
issues: 200,
|
||||||
});
|
});
|
||||||
@@ -116,7 +126,8 @@ describe("Test fetchStats", () => {
|
|||||||
totalCommits: 100,
|
totalCommits: 100,
|
||||||
totalIssues: 200,
|
totalIssues: 200,
|
||||||
totalPRs: 300,
|
totalPRs: 300,
|
||||||
totalStars: 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,
|
rank,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -125,9 +136,9 @@ describe("Test fetchStats", () => {
|
|||||||
mock.reset();
|
mock.reset();
|
||||||
mock
|
mock
|
||||||
.onPost("https://api.github.com/graphql")
|
.onPost("https://api.github.com/graphql")
|
||||||
.replyOnce(200, data_stats)
|
.replyOnce(200, data)
|
||||||
.onPost("https://api.github.com/graphql")
|
.onPost("https://api.github.com/graphql")
|
||||||
.replyOnce(200, data_repo_zero_stars);
|
.replyOnce(200, repositoriesWithZeroStarsData);
|
||||||
|
|
||||||
let stats = await fetchStats("anuraghazra");
|
let stats = await fetchStats("anuraghazra");
|
||||||
const rank = calculateRank({
|
const rank = calculateRank({
|
||||||
@@ -167,7 +178,8 @@ describe("Test fetchStats", () => {
|
|||||||
totalRepos: 5,
|
totalRepos: 5,
|
||||||
followers: 100,
|
followers: 100,
|
||||||
contributions: 61,
|
contributions: 61,
|
||||||
stargazers: 300,
|
// 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,
|
prs: 300,
|
||||||
issues: 200,
|
issues: 200,
|
||||||
});
|
});
|
||||||
@@ -178,7 +190,8 @@ describe("Test fetchStats", () => {
|
|||||||
totalCommits: 150,
|
totalCommits: 150,
|
||||||
totalIssues: 200,
|
totalIssues: 200,
|
||||||
totalPRs: 300,
|
totalPRs: 300,
|
||||||
totalStars: 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,
|
rank,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -194,7 +207,8 @@ describe("Test fetchStats", () => {
|
|||||||
totalRepos: 5,
|
totalRepos: 5,
|
||||||
followers: 100,
|
followers: 100,
|
||||||
contributions: 61,
|
contributions: 61,
|
||||||
stargazers: 300,
|
// 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,
|
prs: 300,
|
||||||
issues: 200,
|
issues: 200,
|
||||||
});
|
});
|
||||||
@@ -205,7 +219,8 @@ describe("Test fetchStats", () => {
|
|||||||
totalCommits: 1050,
|
totalCommits: 1050,
|
||||||
totalIssues: 200,
|
totalIssues: 200,
|
||||||
totalPRs: 300,
|
totalPRs: 300,
|
||||||
totalStars: 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,
|
rank,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -221,7 +236,8 @@ describe("Test fetchStats", () => {
|
|||||||
totalRepos: 5,
|
totalRepos: 5,
|
||||||
followers: 100,
|
followers: 100,
|
||||||
contributions: 61,
|
contributions: 61,
|
||||||
stargazers: 200,
|
// 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,
|
prs: 300,
|
||||||
issues: 200,
|
issues: 200,
|
||||||
});
|
});
|
||||||
@@ -232,82 +248,8 @@ describe("Test fetchStats", () => {
|
|||||||
totalCommits: 1050,
|
totalCommits: 1050,
|
||||||
totalIssues: 200,
|
totalIssues: 200,
|
||||||
totalPRs: 300,
|
totalPRs: 300,
|
||||||
totalStars: 200,
|
// totalStars: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||||
rank,
|
totalStars: 200, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should fetch two pages of stars if 'FETCH_MULTI_PAGE_STARS' env variable is set to `true`", async () => {
|
|
||||||
process.env.FETCH_MULTI_PAGE_STARS = true;
|
|
||||||
|
|
||||||
let stats = await fetchStats("anuraghazra");
|
|
||||||
const rank = calculateRank({
|
|
||||||
totalCommits: 100,
|
|
||||||
totalRepos: 5,
|
|
||||||
followers: 100,
|
|
||||||
contributions: 61,
|
|
||||||
stargazers: 400,
|
|
||||||
prs: 300,
|
|
||||||
issues: 200,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(stats).toStrictEqual({
|
|
||||||
contributedTo: 61,
|
|
||||||
name: "Anurag Hazra",
|
|
||||||
totalCommits: 100,
|
|
||||||
totalIssues: 200,
|
|
||||||
totalPRs: 300,
|
|
||||||
totalStars: 400,
|
|
||||||
rank,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should fetch one page of stars if 'FETCH_MULTI_PAGE_STARS' env variable is set to `false`", async () => {
|
|
||||||
process.env.FETCH_MULTI_PAGE_STARS = "false";
|
|
||||||
|
|
||||||
let stats = await fetchStats("anuraghazra");
|
|
||||||
const rank = calculateRank({
|
|
||||||
totalCommits: 100,
|
|
||||||
totalRepos: 5,
|
|
||||||
followers: 100,
|
|
||||||
contributions: 61,
|
|
||||||
stargazers: 300,
|
|
||||||
prs: 300,
|
|
||||||
issues: 200,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(stats).toStrictEqual({
|
|
||||||
contributedTo: 61,
|
|
||||||
name: "Anurag Hazra",
|
|
||||||
totalCommits: 100,
|
|
||||||
totalIssues: 200,
|
|
||||||
totalPRs: 300,
|
|
||||||
totalStars: 300,
|
|
||||||
rank,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should fetch one page of stars if 'FETCH_MULTI_PAGE_STARS' env variable is not set", async () => {
|
|
||||||
process.env.FETCH_MULTI_PAGE_STARS = undefined;
|
|
||||||
|
|
||||||
let stats = await fetchStats("anuraghazra");
|
|
||||||
const rank = calculateRank({
|
|
||||||
totalCommits: 100,
|
|
||||||
totalRepos: 5,
|
|
||||||
followers: 100,
|
|
||||||
contributions: 61,
|
|
||||||
stargazers: 300,
|
|
||||||
prs: 300,
|
|
||||||
issues: 200,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(stats).toStrictEqual({
|
|
||||||
contributedTo: 61,
|
|
||||||
name: "Anurag Hazra",
|
|
||||||
totalCommits: 100,
|
|
||||||
totalIssues: 200,
|
|
||||||
totalPRs: 300,
|
|
||||||
totalStars: 300,
|
|
||||||
rank,
|
rank,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,244 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file Tests for the status/pat-info cloud function.
|
|
||||||
*/
|
|
||||||
import dotenv from "dotenv";
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
import { jest } from "@jest/globals";
|
|
||||||
import axios from "axios";
|
|
||||||
import MockAdapter from "axios-mock-adapter";
|
|
||||||
import patInfo, { RATE_LIMIT_SECONDS } from "../api/status/pat-info.js";
|
|
||||||
|
|
||||||
const mock = new MockAdapter(axios);
|
|
||||||
|
|
||||||
const successData = {
|
|
||||||
data: {
|
|
||||||
rateLimit: {
|
|
||||||
remaining: 4986,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const faker = (query) => {
|
|
||||||
const req = {
|
|
||||||
query: { ...query },
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: jest.fn(),
|
|
||||||
send: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return { req, res };
|
|
||||||
};
|
|
||||||
|
|
||||||
const rate_limit_error = {
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
type: "RATE_LIMITED",
|
|
||||||
message: "API rate limit exceeded for user ID.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
data: {
|
|
||||||
rateLimit: {
|
|
||||||
resetAt: Date.now(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const other_error = {
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
type: "SOME_ERROR",
|
|
||||||
message: "This is a error",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const bad_credentials_error = {
|
|
||||||
message: "Bad credentials",
|
|
||||||
};
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
mock.reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Test /api/status/pat-info", () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
// reset patenv first so that dotenv doesn't populate them with local envs
|
|
||||||
process.env = {};
|
|
||||||
process.env.PAT_1 = "testPAT1";
|
|
||||||
process.env.PAT_2 = "testPAT2";
|
|
||||||
process.env.PAT_3 = "testPAT3";
|
|
||||||
process.env.PAT_4 = "testPAT4";
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return only 'validPATs' if all PATs are valid", async () => {
|
|
||||||
mock
|
|
||||||
.onPost("https://api.github.com/graphql")
|
|
||||||
.replyOnce(200, rate_limit_error)
|
|
||||||
.onPost("https://api.github.com/graphql")
|
|
||||||
.reply(200, successData);
|
|
||||||
|
|
||||||
const { req, res } = faker({}, {});
|
|
||||||
await patInfo(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
|
||||||
expect(res.send).toBeCalledWith(
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
validPATs: ["PAT_2", "PAT_3", "PAT_4"],
|
|
||||||
expiredPATs: [],
|
|
||||||
exhaustedPATs: ["PAT_1"],
|
|
||||||
suspendedPATs: [],
|
|
||||||
errorPATs: [],
|
|
||||||
details: {
|
|
||||||
PAT_1: {
|
|
||||||
status: "exhausted",
|
|
||||||
remaining: 0,
|
|
||||||
resetIn: "0 minutes",
|
|
||||||
},
|
|
||||||
PAT_2: {
|
|
||||||
status: "valid",
|
|
||||||
remaining: 4986,
|
|
||||||
},
|
|
||||||
PAT_3: {
|
|
||||||
status: "valid",
|
|
||||||
remaining: 4986,
|
|
||||||
},
|
|
||||||
PAT_4: {
|
|
||||||
status: "valid",
|
|
||||||
remaining: 4986,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return `errorPATs` if a PAT causes an error to be thrown", async () => {
|
|
||||||
mock
|
|
||||||
.onPost("https://api.github.com/graphql")
|
|
||||||
.replyOnce(200, other_error)
|
|
||||||
.onPost("https://api.github.com/graphql")
|
|
||||||
.reply(200, successData);
|
|
||||||
|
|
||||||
const { req, res } = faker({}, {});
|
|
||||||
await patInfo(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
|
||||||
expect(res.send).toBeCalledWith(
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
validPATs: ["PAT_2", "PAT_3", "PAT_4"],
|
|
||||||
expiredPATs: [],
|
|
||||||
exhaustedPATs: [],
|
|
||||||
suspendedPATs: [],
|
|
||||||
errorPATs: ["PAT_1"],
|
|
||||||
details: {
|
|
||||||
PAT_1: {
|
|
||||||
status: "error",
|
|
||||||
error: {
|
|
||||||
type: "SOME_ERROR",
|
|
||||||
message: "This is a error",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
PAT_2: {
|
|
||||||
status: "valid",
|
|
||||||
remaining: 4986,
|
|
||||||
},
|
|
||||||
PAT_3: {
|
|
||||||
status: "valid",
|
|
||||||
remaining: 4986,
|
|
||||||
},
|
|
||||||
PAT_4: {
|
|
||||||
status: "valid",
|
|
||||||
remaining: 4986,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return `expiredPaths` if a PAT returns a 'Bad credentials' error", async () => {
|
|
||||||
mock
|
|
||||||
.onPost("https://api.github.com/graphql")
|
|
||||||
.replyOnce(404, bad_credentials_error)
|
|
||||||
.onPost("https://api.github.com/graphql")
|
|
||||||
.reply(200, successData);
|
|
||||||
|
|
||||||
const { req, res } = faker({}, {});
|
|
||||||
await patInfo(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
|
||||||
expect(res.send).toBeCalledWith(
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
validPATs: ["PAT_2", "PAT_3", "PAT_4"],
|
|
||||||
expiredPATs: ["PAT_1"],
|
|
||||||
exhaustedPATs: [],
|
|
||||||
suspendedPATs: [],
|
|
||||||
errorPATs: [],
|
|
||||||
details: {
|
|
||||||
PAT_1: {
|
|
||||||
status: "expired",
|
|
||||||
},
|
|
||||||
PAT_2: {
|
|
||||||
status: "valid",
|
|
||||||
remaining: 4986,
|
|
||||||
},
|
|
||||||
PAT_3: {
|
|
||||||
status: "valid",
|
|
||||||
remaining: 4986,
|
|
||||||
},
|
|
||||||
PAT_4: {
|
|
||||||
status: "valid",
|
|
||||||
remaining: 4986,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw an error if something goes wrong", async () => {
|
|
||||||
mock.onPost("https://api.github.com/graphql").networkError();
|
|
||||||
|
|
||||||
const { req, res } = faker({}, {});
|
|
||||||
await patInfo(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
|
||||||
expect(res.send).toBeCalledWith("Something went wrong: Network Error");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have proper cache when no error is thrown", async () => {
|
|
||||||
mock.onPost("https://api.github.com/graphql").reply(200, successData);
|
|
||||||
|
|
||||||
const { req, res } = faker({}, {});
|
|
||||||
await patInfo(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader.mock.calls).toEqual([
|
|
||||||
["Content-Type", "application/json"],
|
|
||||||
["Cache-Control", `max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have proper cache when error is thrown", async () => {
|
|
||||||
mock.reset();
|
|
||||||
mock.onPost("https://api.github.com/graphql").networkError();
|
|
||||||
|
|
||||||
const { req, res } = faker({}, {});
|
|
||||||
await patInfo(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader.mock.calls).toEqual([
|
|
||||||
["Content-Type", "application/json"],
|
|
||||||
["Cache-Control", "no-store"],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -78,17 +78,16 @@ describe("Test renderStatsCard", () => {
|
|||||||
|
|
||||||
it("should render with custom width set", () => {
|
it("should render with custom width set", () => {
|
||||||
document.body.innerHTML = renderStatsCard(stats);
|
document.body.innerHTML = renderStatsCard(stats);
|
||||||
expect(document.querySelector("svg")).toHaveAttribute("width", "450");
|
expect(document.querySelector("svg")).toHaveAttribute("width", "495");
|
||||||
|
|
||||||
document.body.innerHTML = renderStatsCard(stats, { card_width: 500 });
|
document.body.innerHTML = renderStatsCard(stats, { card_width: 400 });
|
||||||
expect(document.querySelector("svg")).toHaveAttribute("width", "500");
|
expect(document.querySelector("svg")).toHaveAttribute("width", "400");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render with custom width set and limit minimum width", () => {
|
it("should render with custom width set and limit minimum width", () => {
|
||||||
document.body.innerHTML = renderStatsCard(stats, { card_width: 1 });
|
document.body.innerHTML = renderStatsCard(stats, { card_width: 1 });
|
||||||
expect(document.querySelector("svg")).toHaveAttribute("width", "420");
|
expect(document.querySelector("svg")).toHaveAttribute("width", "340");
|
||||||
|
|
||||||
// Test default minimum card width without rank circle.
|
|
||||||
document.body.innerHTML = renderStatsCard(stats, {
|
document.body.innerHTML = renderStatsCard(stats, {
|
||||||
card_width: 1,
|
card_width: 1,
|
||||||
hide_rank: true,
|
hide_rank: true,
|
||||||
@@ -98,7 +97,6 @@ describe("Test renderStatsCard", () => {
|
|||||||
"305.81250000000006",
|
"305.81250000000006",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Test minimum card width with rank and icons.
|
|
||||||
document.body.innerHTML = renderStatsCard(stats, {
|
document.body.innerHTML = renderStatsCard(stats, {
|
||||||
card_width: 1,
|
card_width: 1,
|
||||||
hide_rank: true,
|
hide_rank: true,
|
||||||
@@ -106,24 +104,22 @@ describe("Test renderStatsCard", () => {
|
|||||||
});
|
});
|
||||||
expect(document.querySelector("svg")).toHaveAttribute(
|
expect(document.querySelector("svg")).toHaveAttribute(
|
||||||
"width",
|
"width",
|
||||||
"322.81250000000006",
|
"305.81250000000006",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Test minimum card width with icons but without rank.
|
|
||||||
document.body.innerHTML = renderStatsCard(stats, {
|
document.body.innerHTML = renderStatsCard(stats, {
|
||||||
card_width: 1,
|
card_width: 1,
|
||||||
hide_rank: false,
|
hide_rank: false,
|
||||||
show_icons: true,
|
show_icons: true,
|
||||||
});
|
});
|
||||||
expect(document.querySelector("svg")).toHaveAttribute("width", "437");
|
expect(document.querySelector("svg")).toHaveAttribute("width", "356");
|
||||||
|
|
||||||
// Test minimum card width without icons or rank.
|
|
||||||
document.body.innerHTML = renderStatsCard(stats, {
|
document.body.innerHTML = renderStatsCard(stats, {
|
||||||
card_width: 1,
|
card_width: 1,
|
||||||
hide_rank: false,
|
hide_rank: false,
|
||||||
show_icons: false,
|
show_icons: false,
|
||||||
});
|
});
|
||||||
expect(document.querySelector("svg")).toHaveAttribute("width", "420");
|
expect(document.querySelector("svg")).toHaveAttribute("width", "340");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render default colors properly", () => {
|
it("should render default colors properly", () => {
|
||||||
@@ -316,7 +312,7 @@ describe("Test renderStatsCard", () => {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
document.body.getElementsByTagName("svg")[0].getAttribute("width"),
|
document.body.getElementsByTagName("svg")[0].getAttribute("width"),
|
||||||
).toBe("287");
|
).toBe("270");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render translations", () => {
|
it("should render translations", () => {
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file Tests for the status/up cloud function.
|
|
||||||
*/
|
|
||||||
import { jest } from "@jest/globals";
|
|
||||||
import axios from "axios";
|
|
||||||
import MockAdapter from "axios-mock-adapter";
|
|
||||||
import up, { RATE_LIMIT_SECONDS } from "../api/status/up.js";
|
|
||||||
|
|
||||||
const mock = new MockAdapter(axios);
|
|
||||||
|
|
||||||
const successData = {
|
|
||||||
rateLimit: {
|
|
||||||
remaining: 4986,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const faker = (query) => {
|
|
||||||
const req = {
|
|
||||||
query: { ...query },
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: jest.fn(),
|
|
||||||
send: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return { req, res };
|
|
||||||
};
|
|
||||||
|
|
||||||
const rate_limit_error = {
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
type: "RATE_LIMITED",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const bad_credentials_error = {
|
|
||||||
message: "Bad credentials",
|
|
||||||
};
|
|
||||||
|
|
||||||
const shields_up = {
|
|
||||||
schemaVersion: 1,
|
|
||||||
label: "Public Instance",
|
|
||||||
isError: true,
|
|
||||||
message: "up",
|
|
||||||
color: "brightgreen",
|
|
||||||
};
|
|
||||||
const shields_down = {
|
|
||||||
schemaVersion: 1,
|
|
||||||
label: "Public Instance",
|
|
||||||
isError: true,
|
|
||||||
message: "down",
|
|
||||||
color: "red",
|
|
||||||
};
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
mock.reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Test /api/status/up", () => {
|
|
||||||
it("should return `true` if request was successful", async () => {
|
|
||||||
mock.onPost("https://api.github.com/graphql").replyOnce(200, successData);
|
|
||||||
|
|
||||||
const { req, res } = faker({}, {});
|
|
||||||
await up(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
|
||||||
expect(res.send).toBeCalledWith(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return `false` if all PATs are rate limited", async () => {
|
|
||||||
mock.onPost("https://api.github.com/graphql").reply(200, rate_limit_error);
|
|
||||||
|
|
||||||
const { req, res } = faker({}, {});
|
|
||||||
await up(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
|
||||||
expect(res.send).toBeCalledWith(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return JSON `true` if request was successful and type='json'", async () => {
|
|
||||||
mock.onPost("https://api.github.com/graphql").replyOnce(200, successData);
|
|
||||||
|
|
||||||
const { req, res } = faker({ type: "json" }, {});
|
|
||||||
await up(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
|
||||||
expect(res.send).toBeCalledWith({ up: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return JSON `false` if all PATs are rate limited and type='json'", async () => {
|
|
||||||
mock.onPost("https://api.github.com/graphql").reply(200, rate_limit_error);
|
|
||||||
|
|
||||||
const { req, res } = faker({ type: "json" }, {});
|
|
||||||
await up(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
|
||||||
expect(res.send).toBeCalledWith({ up: false });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return UP shields.io config if request was successful and type='shields'", async () => {
|
|
||||||
mock.onPost("https://api.github.com/graphql").replyOnce(200, successData);
|
|
||||||
|
|
||||||
const { req, res } = faker({ type: "shields" }, {});
|
|
||||||
await up(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
|
||||||
expect(res.send).toBeCalledWith(shields_up);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return DOWN shields.io config if all PATs are rate limited and type='shields'", async () => {
|
|
||||||
mock.onPost("https://api.github.com/graphql").reply(200, rate_limit_error);
|
|
||||||
|
|
||||||
const { req, res } = faker({ type: "shields" }, {});
|
|
||||||
await up(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
|
||||||
expect(res.send).toBeCalledWith(shields_down);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return `true` if the first PAT is rate limited but the second PATs works", async () => {
|
|
||||||
mock
|
|
||||||
.onPost("https://api.github.com/graphql")
|
|
||||||
.replyOnce(200, rate_limit_error)
|
|
||||||
.onPost("https://api.github.com/graphql")
|
|
||||||
.replyOnce(200, successData);
|
|
||||||
|
|
||||||
const { req, res } = faker({}, {});
|
|
||||||
await up(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
|
||||||
expect(res.send).toBeCalledWith(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return `true` if the first PAT has 'Bad credentials' but the second PAT works", async () => {
|
|
||||||
mock
|
|
||||||
.onPost("https://api.github.com/graphql")
|
|
||||||
.replyOnce(404, bad_credentials_error)
|
|
||||||
.onPost("https://api.github.com/graphql")
|
|
||||||
.replyOnce(200, successData);
|
|
||||||
|
|
||||||
const { req, res } = faker({}, {});
|
|
||||||
await up(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
|
||||||
expect(res.send).toBeCalledWith(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return `false` if all pats have 'Bad credentials'", async () => {
|
|
||||||
mock
|
|
||||||
.onPost("https://api.github.com/graphql")
|
|
||||||
.reply(404, bad_credentials_error);
|
|
||||||
|
|
||||||
const { req, res } = faker({}, {});
|
|
||||||
await up(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
|
||||||
expect(res.send).toBeCalledWith(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw an error if the request fails", async () => {
|
|
||||||
mock.onPost("https://api.github.com/graphql").networkError();
|
|
||||||
|
|
||||||
const { req, res } = faker({}, {});
|
|
||||||
await up(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
|
||||||
expect(res.send).toBeCalledWith(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have proper cache when no error is thrown", async () => {
|
|
||||||
mock.onPost("https://api.github.com/graphql").replyOnce(200, successData);
|
|
||||||
|
|
||||||
const { req, res } = faker({}, {});
|
|
||||||
await up(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader.mock.calls).toEqual([
|
|
||||||
["Content-Type", "application/json"],
|
|
||||||
["Cache-Control", `max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have proper cache when error is thrown", async () => {
|
|
||||||
mock.onPost("https://api.github.com/graphql").networkError();
|
|
||||||
|
|
||||||
const { req, res } = faker({}, {});
|
|
||||||
await up(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader.mock.calls).toEqual([
|
|
||||||
["Content-Type", "application/json"],
|
|
||||||
["Cache-Control", "no-store"],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -374,13 +374,6 @@ export const themes = {
|
|||||||
border_color: "170F0C",
|
border_color: "170F0C",
|
||||||
bg_color: "170F0C",
|
bg_color: "170F0C",
|
||||||
},
|
},
|
||||||
rose: {
|
|
||||||
title_color: "8d192b",
|
|
||||||
text_color: "862931",
|
|
||||||
icon_color: "B71F36",
|
|
||||||
border_color: "e9d8d4",
|
|
||||||
bg_color: "e9d8d4",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default themes;
|
export default themes;
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"functions": {
|
"functions": {
|
||||||
"api/*.js": {
|
"api/*.js": {
|
||||||
"memory": 128,
|
"memory": 128,
|
||||||
"maxDuration": 10
|
"maxDuration": 30
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"redirects": [
|
"redirects": [
|
||||||
|
|||||||
Reference in New Issue
Block a user