forked from mirrored/github-readme-stats
Compare commits
77
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ab959d119 | ||
|
|
0caa4c5fd8 | ||
|
|
0c2fe4e07b | ||
|
|
a340900e8a | ||
|
|
2619c12638 | ||
|
|
d306bec73e | ||
|
|
c656f3df6b | ||
|
|
c6dab8b0f2 | ||
|
|
311204f4f9 | ||
|
|
0dc3531666 | ||
|
|
a4d6fe104f | ||
|
|
cb99414d3d | ||
|
|
2f4279e738 | ||
|
|
3bd6519d39 | ||
|
|
31d1ab43d0 | ||
|
|
a82a617a37 | ||
|
|
6eebfe36e8 | ||
|
|
30a45d3c13 | ||
|
|
fd64333211 | ||
|
|
f5f0a79bc1 | ||
|
|
5577bbf07f | ||
|
|
da46a5093c | ||
|
|
21a9ba4fae | ||
|
|
8aacec098b | ||
|
|
da5f82ff02 | ||
|
|
be0d96615d | ||
|
|
aec73d293e | ||
|
|
879937c11d | ||
|
|
4d1d83d5e5 | ||
|
|
d8244a7fe5 | ||
|
|
b928f51442 | ||
|
|
32998295b7 | ||
|
|
2bd9d457ac | ||
|
|
c5063b92b6 | ||
|
|
b93aee34d0 | ||
|
|
ed18914fa4 | ||
|
|
1e61f9f3fe | ||
|
|
976771080f | ||
|
|
7bc8f19a7f | ||
|
|
9ec2c8367a | ||
|
|
a1c3c6accc | ||
|
|
8849b5f5fc | ||
|
|
d60d53cdb4 | ||
|
|
252c2b419d | ||
|
|
d5fbfb4345 | ||
|
|
82224fa68a | ||
|
|
1d528da1dc | ||
|
|
2ab8b85ae3 | ||
|
|
a3c6f874af | ||
|
|
8898d013b6 | ||
|
|
91345ed55f | ||
|
|
55a303b4a6 | ||
|
|
f3f7a4837d | ||
|
|
a6ff0fa521 | ||
|
|
5f20e6c97a | ||
|
|
ba7c2f8b55 | ||
|
|
888663a477 | ||
|
|
112000667c | ||
|
|
7aa502d453 | ||
|
|
077d40561a | ||
|
|
99d9d3cde0 | ||
|
|
cd5cbcdb09 | ||
|
|
b2bf4fa487 | ||
|
|
4b17300126 | ||
|
|
a17fa1cf5d | ||
|
|
8667ea9908 | ||
|
|
60fae292a3 | ||
|
|
c1dc7b850c | ||
|
|
4ff2c2a425 | ||
|
|
fbb49e3c80 | ||
|
|
eab1402417 | ||
|
|
cfa84232e2 | ||
|
|
d5d1f44e1c | ||
|
|
8bc69e761f | ||
|
|
06a2a78cfc | ||
|
|
ad9db67b4c | ||
|
|
ab9c563ec5 |
@@ -1,3 +1,4 @@
|
||||
themes: themes/index.js
|
||||
doc-translation: docs/*
|
||||
card-i18n: src/translations.js
|
||||
documentation: readme.md
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
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()
|
||||
@@ -0,0 +1,20 @@
|
||||
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,6 +5,7 @@ on:
|
||||
jobs:
|
||||
e2eTests:
|
||||
if:
|
||||
github.repository == 'anuraghazra/github-readme-stats' &&
|
||||
github.event_name == 'deployment_status' &&
|
||||
github.event.deployment_status.state == 'success'
|
||||
name: Perform 2e2 tests
|
||||
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
closeEmptyIssuesAndTemplates:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
name: Close empty issues
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -23,6 +23,10 @@ jobs:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
|
||||
# Fix the unsafe repo error which was introduced by the CVE-2022-24765 git patches.
|
||||
- name: Fix unsafe repo error
|
||||
run: git config --global --add safe.directory ${{ github.workspace }}
|
||||
|
||||
- name: npm install, generate readme
|
||||
run: |
|
||||
npm ci
|
||||
|
||||
@@ -4,6 +4,7 @@ on:
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
name: prs cache clean
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Cleanup
|
||||
run: |
|
||||
gh extension install actions/gh-actions-cache
|
||||
|
||||
REPO=${{ github.repository }}
|
||||
BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge"
|
||||
|
||||
echo "Fetching list of cache key"
|
||||
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
|
||||
|
||||
## Setting this to not fail the workflow while deleting cache keys.
|
||||
set +e
|
||||
echo "Deleting caches..."
|
||||
for cacheKey in $cacheKeysForPR
|
||||
do
|
||||
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
|
||||
done
|
||||
echo "Done"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
|
||||
jobs:
|
||||
closeOldThemePrs:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
name: Close stale 'invalid' theme PRs
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
|
||||
@@ -34,4 +34,4 @@ jobs:
|
||||
npm run format:check
|
||||
|
||||
- name: Code Coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
|
||||
jobs:
|
||||
showAndLabelTopIssues:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
name: Update top issues Dashboard.
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
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"
|
||||
@@ -10,3 +10,5 @@ vercel_token
|
||||
# IDE
|
||||
.vscode
|
||||
*.code-workspace
|
||||
|
||||
.vercel
|
||||
|
||||
+12
-8
@@ -2,11 +2,11 @@
|
||||
|
||||
We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
|
||||
|
||||
- Reporting an issue
|
||||
- Discussing the current state of the code
|
||||
- Submitting a fix
|
||||
- Proposing new features
|
||||
- Becoming a maintainer
|
||||
- Reporting [an issue](https://github.com/anuraghazra/github-readme-stats/issues/new?assignees=&labels=bug&template=bug_report.yml).
|
||||
- [Discussing](https://github.com/anuraghazra/github-readme-stats/discussions) the current state of the code.
|
||||
- Submitting [a fix](https://github.com/anuraghazra/github-readme-stats/compare).
|
||||
- Proposing [new features](https://github.com/anuraghazra/github-readme-stats/issues/new?assignees=&labels=enhancement&template=feature_request.yml).
|
||||
- Becoming a maintainer.
|
||||
|
||||
## All Changes Happen Through Pull Requests
|
||||
|
||||
@@ -33,11 +33,15 @@ _(make sure you already have a [Vercel](https://vercel.com/) account)_
|
||||
1. Install [Vercel CLI](https://vercel.com/download).
|
||||
2. Fork the repository and clone the code to your local machine.
|
||||
3. Run `npm install` in the repository root.
|
||||
4. Run the command "vercel" in the root and follow the steps there.
|
||||
4. Run the command `vercel` in the root and follow the steps there.
|
||||
5. Open `vercel.json` and set the maxDuration to 10.
|
||||
6. Create a `.env` file in the root of the directory.
|
||||
7. In the .env file add a new variable named "PAT_1" with your [GitHub Personal Access Token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token).
|
||||
8. Run the command "vercel dev" to start a development server at <https://localhost:3000>.
|
||||
7. In the .env file add a new variable named `PAT_1` with your [GitHub Personal Access Token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token).
|
||||
8. Run the command `vercel dev` to start a development server at <https://localhost:3000>.
|
||||
9. The cards will then be available from this local endpoint (i.e. `https://localhost:3000/api?username=anuraghazra`).
|
||||
|
||||
> **Note**
|
||||
> You can also debug any tests using the [VSCode Jest extension](https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest). For more information see https://github.com/jest-community/vscode-jest/issues/912.
|
||||
|
||||
## Themes Contribution
|
||||
|
||||
|
||||
@@ -35,7 +35,9 @@ export default async (req, res) => {
|
||||
locale,
|
||||
disable_animations,
|
||||
border_radius,
|
||||
number_format,
|
||||
border_color,
|
||||
rank_icon,
|
||||
} = req.query;
|
||||
res.setHeader("Content-Type", "image/svg+xml");
|
||||
|
||||
@@ -88,8 +90,10 @@ export default async (req, res) => {
|
||||
custom_title,
|
||||
border_radius,
|
||||
border_color,
|
||||
number_format,
|
||||
locale: locale ? locale.toLowerCase() : null,
|
||||
disable_animations: parseBoolean(disable_animations),
|
||||
rank_icon,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @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 5 minutes.
|
||||
*/
|
||||
|
||||
import { logger, request, dateDiff } from "../../src/common/utils.js";
|
||||
export const RATE_LIMIT_SECONDS = 60 * 5; // 1 request per 5 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);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* @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 5 minutes.
|
||||
*/
|
||||
|
||||
import retryer from "../../src/common/retryer.js";
|
||||
import { logger, request } from "../../src/common/utils.js";
|
||||
|
||||
export const RATE_LIMIT_SECONDS = 60 * 5; // 1 request per 5 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);
|
||||
}
|
||||
};
|
||||
@@ -25,10 +25,14 @@ export default async (req, res) => {
|
||||
layout,
|
||||
langs_count,
|
||||
exclude_repo,
|
||||
size_weight,
|
||||
count_weight,
|
||||
custom_title,
|
||||
locale,
|
||||
border_radius,
|
||||
border_color,
|
||||
disable_animations,
|
||||
hide_progress,
|
||||
} = req.query;
|
||||
res.setHeader("Content-Type", "image/svg+xml");
|
||||
|
||||
@@ -44,6 +48,8 @@ export default async (req, res) => {
|
||||
const topLangs = await fetchTopLanguages(
|
||||
username,
|
||||
parseArray(exclude_repo),
|
||||
size_weight,
|
||||
count_weight,
|
||||
);
|
||||
|
||||
const cacheSeconds = clampValue(
|
||||
@@ -75,6 +81,8 @@ export default async (req, res) => {
|
||||
border_radius,
|
||||
border_color,
|
||||
locale: locale ? locale.toLowerCase() : null,
|
||||
disable_animations: parseBoolean(disable_animations),
|
||||
hide_progress: parseBoolean(hide_progress),
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
|
||||
+3
-1
@@ -53,6 +53,8 @@
|
||||
<a href="/docs/readme_nl.md">Nederlands</a>
|
||||
.
|
||||
<a href="/docs/readme_np.md">नेपाली</a>
|
||||
.
|
||||
<a href="/docs/readme_tr.md">Türkçe</a>
|
||||
</p>
|
||||
</p>
|
||||
<p align="center">喜欢这个项目?请考虑<a href="https://www.paypal.me/anuraghazra">捐赠</a>来帮助它完善!
|
||||
@@ -138,7 +140,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr
|
||||
- `bg_color` - 卡片背景颜色 _(十六进制色码)_ **或者** 以 _angle,start,end_ 的形式渐变
|
||||
- `hide_border` - 隐藏卡的边框 _(布尔值)_
|
||||
- `theme` - 主题名称,从[所有可用主题](../themes/README.md)中选择
|
||||
- `cache_seconds` - 手动设置缓存头 _(最小值: 1800,最大值: 86400)_
|
||||
- `cache_seconds` - 手动设置缓存头 _(最小值: 14400,最大值: 86400)_
|
||||
- `locale` - 在卡片中设置语言 _(例如 cn, de, es, 等等)_
|
||||
|
||||
##### bg_color 渐变
|
||||
|
||||
+3
-1
@@ -54,6 +54,8 @@
|
||||
<a href="/docs/readme_nl.md">Nederlands</a>
|
||||
.
|
||||
<a href="/docs/readme_np.md">नेपाली</a>
|
||||
.
|
||||
<a href="/docs/readme_tr.md">Türkçe</a>
|
||||
</p>
|
||||
</p>
|
||||
<p align="center">Du magst das Projekt? Wie wäre es mit einer kleinen <a href="https://www.paypal.me/anuraghazra">Spende</a> um es weiterhin am Leben zu erhalten?
|
||||
@@ -128,7 +130,7 @@ Du kannst das Erscheinungsbild deiner `Stats Card` oder `Repo Card`, mithilfe vo
|
||||
- `bg_color` - Hintergrundfarbe _(hex color)_ **oder** ein Farbverlauf in der Form von _winkel,start,ende_
|
||||
- `hide_border` - Blendet den Rand der Karte aus _(Boolean)_
|
||||
- `theme` - Name des Erscheinungsbildes/Themes [alle verfügbaren Themes](../themes/README.md)
|
||||
- `cache_seconds` - manuelles festlegen der Cachezeiten _(min: 1800, max: 86400)_
|
||||
- `cache_seconds` - manuelles festlegen der Cachezeiten _(min: 14400, max: 86400)_
|
||||
- `locale` - Stellen Sie die Sprache auf der Karte ein _(z.B. cn, de, es, etc.)_
|
||||
|
||||
##### Farbverlauf in bg_color
|
||||
|
||||
+1
-1
@@ -142,7 +142,7 @@ Puedes personalizar el aspecto de tu `Tarjeta de Estadísticas` o `Tarjeta de Re
|
||||
- `bg_color` - Color de fondo _(hex color)_
|
||||
- `hide_border` - Oculta el borde de la tarjeta _(booleano)_
|
||||
- `theme` - Nombre del tema, elige uno de [todos los temas disponible ](../themes/README.md)
|
||||
- `cache_seconds` - Cache _(min: 1800, max: 86400)_
|
||||
- `cache_seconds` - Cache _(min: 14400, max: 86400)_
|
||||
- `locale` - configurar el idioma en la tarjeta _(p.ej. cn, de, es, etc.)_
|
||||
|
||||
##### Gradiente en `bg_color`
|
||||
|
||||
+3
-1
@@ -53,6 +53,8 @@
|
||||
<a href="/docs/readme_nl.md">Nederlands</a>
|
||||
.
|
||||
<a href="/docs/readme_np.md">नेपाली</a>
|
||||
.
|
||||
<a href="/docs/readme_tr.md">Türkçe</a>
|
||||
</p>
|
||||
</p>
|
||||
<p align="center">Vous aimez ce projet? Pensez <a href="https://www.paypal.me/anuraghazra">à faire un don</a> pour l'améliorer!
|
||||
@@ -138,7 +140,7 @@ Vous pouvez personnaliser l'apparence de votre `Carte des stats` ou `Carte de d
|
||||
- `bg_color` - Couleur du fond de la carte _(hex color)_ **ou** un gradiant de la forme _angle,start,end_
|
||||
- `hide_border` - Cache la bordure de la carte _(booléen)_
|
||||
- `theme` - Nom du thème, parmis [tous les thèmes disponibles](../themes/README.md)
|
||||
- `cache_seconds` - Paramétrer le cache manuellement _(min: 1800, max: 86400)_
|
||||
- `cache_seconds` - Paramétrer le cache manuellement _(min: 14400, max: 86400)_
|
||||
- `locale` - définir la langue de la carte _(par exemple. cn, de, es, etc.)_
|
||||
|
||||
##### Gradient in bg_color
|
||||
|
||||
+3
-1
@@ -53,6 +53,8 @@
|
||||
<a href="/docs/readme_nl.md">Nederlands</a>
|
||||
.
|
||||
<a href="/docs/readme_np.md">नेपाली</a>
|
||||
.
|
||||
<a href="/docs/readme_tr.md">Türkçe</a>
|
||||
</p>
|
||||
</p>
|
||||
<p align="center">Se ti piace questo progetto, considera la possibilità di <a href="https://www.paypal.me/anuraghazra">donare</a> per aiutare a renderlo migliore!
|
||||
@@ -138,7 +140,7 @@ Puoi personalizzare l'aspetto delle tue `Stats Card` o delle `Repo Card` in qual
|
||||
- `bg_color` - Colore dello sfondo _(in esadecimale)_ **oppure** un gradiente nella forma _angolo,inizio,fine_
|
||||
- `hide_border` - Nasconde il bordo della carta _(booleano)_
|
||||
- `theme` - Nome del tema, dai un'occhiata a [tutti i temi disponibili](../themes/README.md)
|
||||
- `cache_seconds` - Specifica manualmente il valore di cache, in secondi _(min: 1800, max: 86400)_
|
||||
- `cache_seconds` - Specifica manualmente il valore di cache, in secondi _(min: 14400, max: 86400)_
|
||||
- `locale` - Impostare la lingua nella scheda _(per esempio. cn, de, es, eccetera.)_
|
||||
|
||||
##### Gradiente nello sfondo
|
||||
|
||||
+3
-1
@@ -53,6 +53,8 @@
|
||||
<a href="/docs/readme_nl.md">Nederlands</a>
|
||||
.
|
||||
<a href="/docs/readme_np.md">नेपाली</a>
|
||||
.
|
||||
<a href="/docs/readme_tr.md">Türkçe</a>
|
||||
</p>
|
||||
</p>
|
||||
<p align="center">このプロジェクトを気に入っていただけましたか?<br>もしよろしければ、プロジェクトのさらなる改善のために<a href="https://www.paypal.me/anuraghazra">寄付</a>を検討して頂けると嬉しいです!</p>
|
||||
@@ -139,7 +141,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr
|
||||
- `bg_color` - 背景の色 _(16 進数カラーコード)_ **または** _angle,start,end_ の形式でグラデーションを指定することも可
|
||||
- `hide_border` - カードの境界線を非表示にします _(ブール値)_
|
||||
- `theme` - [使用可能なテーマ一覧](../themes/README.md) から選んだテーマ名
|
||||
- `cache_seconds` - キャッシュ時間の秒数 _(最小値: 1800, 最大値: 86400)_
|
||||
- `cache_seconds` - キャッシュ時間の秒数 _(最小値: 14400, 最大値: 86400)_
|
||||
- `locale` - カードに言語を設定する _(例えば cn, de, es, 等)_
|
||||
|
||||
##### bg_color の グラデーション指定
|
||||
|
||||
+3
-1
@@ -53,6 +53,8 @@
|
||||
<a href="/docs/readme_nl.md">Nederlands</a>
|
||||
.
|
||||
<a href="/docs/readme_np.md">नेपाली</a>
|
||||
.
|
||||
<a href="/docs/readme_tr.md">Türkçe</a>
|
||||
</p>
|
||||
</p>
|
||||
<p align="center">기능들이 마음에 드시나요? 괜찮으시다면, 서비스 개선을 위해 <a href="https://www.paypal.me/anuraghazra">기부</a>를 고려해주세요!
|
||||
@@ -149,7 +151,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr
|
||||
- `bg_color` - 카드의 배경 색상 _(hex color)_ **혹은** 다음 양식으로 그라데이션 주기 _angle,start,end_
|
||||
- `hide_border` - 카드의 테두리 표시 여부 _(boolean)_
|
||||
- `theme` - 테마의 이름, [사용 가능한 모든 테마](../themes/README.md) 에서 선택
|
||||
- `cache_seconds` - 수동으로 캐시 헤더 설정 _(min: 1800, max: 86400)_
|
||||
- `cache_seconds` - 수동으로 캐시 헤더 설정 _(min: 14400, max: 86400)_
|
||||
- `locale` - 카드에 표시할 언어 _(e.g. kr, cn, de, es, etc.)_
|
||||
|
||||
##### 배경에 그라데이션 주기
|
||||
|
||||
+3
-1
@@ -53,6 +53,8 @@
|
||||
<a href="/docs/readme_nl.md">Nederlands</a>
|
||||
.
|
||||
<a href="/docs/readme_np.md">नेपाली</a>
|
||||
.
|
||||
<a href="/docs/readme_tr.md">Türkçe</a>
|
||||
</p>
|
||||
</p>
|
||||
<p align="center">Bevalt het project? <a href="https://www.paypal.me/anuraghazra">Doneer</a> om het te verbeteren!
|
||||
@@ -142,7 +144,7 @@ Je kan het uiterlijk van je `Statistieken kaart` of `Repo kaart` aanpassen hoe j
|
||||
- `bg_color` - Achtergrond kleur van de kaart _(hex kleur)_ **of** een verloop van kleuren in het formaat van _graden,start,einde_
|
||||
- `hide_border` - Verbergt de rand van de kaart _(boolean)_
|
||||
- `theme` - Naam van het thema, kies uit [alle beschikbare thema\'s](../themes/README.md)
|
||||
- `cache_seconds` - Stel de cache header handmatig in _(min: 1800, max: 86400)_
|
||||
- `cache_seconds` - Stel de cache header handmatig in _(min: 14400, max: 86400)_
|
||||
- `locale` - Stel taal van de kaart in _(e.g. cn, de, es, etc.)_
|
||||
|
||||
##### Kleurenverloop in bg_color (achtergrond kleur):
|
||||
|
||||
+5
-1
@@ -49,8 +49,12 @@
|
||||
<a href="/docs/readme_it.md">Italiano</a>
|
||||
·
|
||||
<a href="/docs/readme_kr.md">한국어</a>
|
||||
.
|
||||
<a href="/docs/readme_nl.md">Nederlands</a>
|
||||
·
|
||||
<a href="/docs/readme_np.md">नेपाली</a>
|
||||
.
|
||||
<a href="/docs/readme_tr.md">Türkçe</a>
|
||||
</p>
|
||||
</p>
|
||||
<p align="center">परियोजना मनपर्यो? तपाईं मद्दत गर्न सक्नुहुन्छ <a href="https://www.paypal.me/anuraghazra">यो परियोजना</a> बढ्न
|
||||
@@ -138,7 +142,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr
|
||||
- `bg_color` - Card's background color _(hex color)_ **or** a gradient in the form of _angle,start,end_
|
||||
- `hide_border` - Hides the card's border _(boolean)_
|
||||
- `theme` - name of the theme, choose from [all available themes](./themes/README.md)
|
||||
- `cache_seconds` - set the cache header manually _(min: 1800, max: 86400)_
|
||||
- `cache_seconds` - set the cache header manually _(min: 14400, max: 86400)_
|
||||
- `locale` - set the language in the card _(e.g. cn, de, es, etc.)_
|
||||
|
||||
##### Gradient in bg_color
|
||||
|
||||
@@ -141,7 +141,7 @@ Personalize a aparência do seu `Stats Card` ou `Repo Card` da maneira que desej
|
||||
- `bg_color` - Cor de fundo do cartão _(hex color)_
|
||||
- `hide_border` - Esconde a borda do cartão _(boleano)_
|
||||
- `theme` - Nome do tema, escolha em [todos os temas disponíveis](../themes/README.md)
|
||||
- `cache_seconds` - Defina o cabeçalho do cache manualmente _(min: 1800, max: 86400)_
|
||||
- `cache_seconds` - Defina o cabeçalho do cache manualmente _(min: 14400, max: 86400)_
|
||||
- `locale` - defina o idioma no cartão _(por exemplo. cn, de, es, etc.)_
|
||||
|
||||
> Nota sobre o cache: Cartões de repositório tem um cache padrão de 30 minutos (1800 segundos), se o número a contagem de forks e contagem de estrelas é menor que 1 mil o padrão é 2 horas (7200 segundos). Note também que o cache é limitado a um mínimo de 30 minutos e um máximo de 24 horas.
|
||||
|
||||
+2
-2
@@ -143,7 +143,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr
|
||||
- `bg_color` - Kartın arkaplan rengi _(hex color / hex rengi)_ **ya da** gradient şeklinde _açı,başlangıç,bitiş_
|
||||
- `hide_border` - Kartın çerçevelerini gizler _(boolean)_
|
||||
- `theme` - Temanın rengi [tüm temalar](./themes/README.md)
|
||||
- `cache_seconds` - Manuel olarak cache'i belirleyebilirsiniz _(en az: 1800, en fazla: 86400)_
|
||||
- `cache_seconds` - Manuel olarak cache'i belirleyebilirsiniz _(en az: 14400, en fazla: 86400)_
|
||||
- `locale` - Karttaki dili seçebilirsiniz _(örneğin; tr, cn, de, es, vb.)_
|
||||
|
||||
##### bg_color'da Gradient
|
||||
@@ -409,7 +409,7 @@ Teşekkürler! :heart:
|
||||
|
||||
---
|
||||
|
||||
[](https://vercel.com?utm_source=github_readme_stats_team&utm_campaign=oss)
|
||||
[](https://vercel.com?utm_source=github_readme_stats_team&utm_campaign=oss)
|
||||
|
||||
|
||||
Katkılara açığız! <3
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import statsCard from "./api/index.js";
|
||||
import repoCard from "./api/pin.js";
|
||||
import langCard from "./api/top-langs.js";
|
||||
import wakatimeCard from "./api/wakatime.js";
|
||||
import express from "express";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
const app = express();
|
||||
app.listen(process.env.port || 9000);
|
||||
|
||||
app.get("/", statsCard);
|
||||
app.get("/pin", repoCard);
|
||||
app.get("/top-langs", langCard);
|
||||
app.get("/wakatime", wakatimeCard);
|
||||
Generated
+7
-7
@@ -22,7 +22,7 @@
|
||||
"@testing-library/dom": "^8.17.1",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@uppercod/css-to-object": "^1.1.1",
|
||||
"axios-mock-adapter": "^1.18.1",
|
||||
"axios-mock-adapter": "^1.21.2",
|
||||
"color-contrast-checker": "^2.1.0",
|
||||
"hjson": "^3.2.2",
|
||||
"husky": "^8.0.0",
|
||||
@@ -5581,9 +5581,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz",
|
||||
"integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==",
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz",
|
||||
"integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
@@ -9855,9 +9855,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"yaml": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz",
|
||||
"integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==",
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz",
|
||||
"integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==",
|
||||
"dev": true
|
||||
},
|
||||
"yargs": {
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@
|
||||
"@testing-library/dom": "^8.17.1",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@uppercod/css-to-object": "^1.1.1",
|
||||
"axios-mock-adapter": "^1.18.1",
|
||||
"axios-mock-adapter": "^1.21.2",
|
||||
"color-contrast-checker": "^2.1.0",
|
||||
"hjson": "^3.2.2",
|
||||
"husky": "^8.0.0",
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
<p align="center">Love the project? Please consider <a href="https://www.paypal.me/anuraghazra">donating</a> to help it improve!</p>
|
||||
|
||||
<a href="https://indiafightscorona.giveindia.org">
|
||||
<img src="https://indiaspora.org/wp-content/uploads/2021/04/give-India-logo.png" alt="Give india logo" width="200" />
|
||||
<img src="https://cfstatic.give.do/logo.png" alt="Give india logo" width="200" />
|
||||
</a>
|
||||
|
||||
Are you considering supporting the project by donating? Please DO NOT!!
|
||||
@@ -92,8 +92,10 @@ Visit <https://indiafightscorona.giveindia.org> and make a small donation to hel
|
||||
- [Repo Card Exclusive Options](#repo-card-exclusive-options)
|
||||
- [Language Card Exclusive Options](#language-card-exclusive-options)
|
||||
- [Wakatime Card Exclusive Option](#wakatime-card-exclusive-options)
|
||||
- [Deploy Yourself](#deploy-on-your-own-vercel-instance)
|
||||
- [Keep your fork up to date](#keep-your-fork-up-to-date)
|
||||
- [Deploy Yourself](#deploy-on-your-own)
|
||||
- [On Vercel](#on-vercel)
|
||||
- [On other platforms](#on-other-platforms)
|
||||
- [Keep your fork up to date](#keep-your-fork-up-to-date)
|
||||
|
||||
# GitHub Stats Card
|
||||
|
||||
@@ -133,7 +135,7 @@ You can add the count of all your private contributions to the total commits cou
|
||||
|
||||
### Showing icons
|
||||
|
||||
To enable icons, you can pass `show_icons=true` in the query param, like so:
|
||||
To enable icons, you can pass `&show_icons=true` in the query param, like so:
|
||||
|
||||
```md
|
||||

|
||||
@@ -181,7 +183,7 @@ We have included a `transparent` theme that has a transparent background. This t
|
||||
|
||||
##### Add transparent alpha channel to a themes bg_color
|
||||
|
||||
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 color with a transparent alpha channel (i.e. `bg_color=00000000`):
|
||||
|
||||
```md
|
||||

|
||||
@@ -214,10 +216,10 @@ You can use [GitHub's theme context](https://github.blog/changelog/2021-11-24-sp
|
||||
##### Use GitHub's new media feature
|
||||
|
||||
You can use [GitHub's new media feature](https://github.blog/changelog/2022-05-19-specify-theme-context-for-images-in-markdown-beta/) in HTML to specify whether to display images for light or dark themes. This is done using the HTML `<picture>` element in combination with the `prefers-color-scheme` media feature.
|
||||
|
||||
|
||||
```html
|
||||
<picture>
|
||||
<source
|
||||
<source
|
||||
srcset="https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
@@ -233,7 +235,7 @@ You can use [GitHub's new media feature](https://github.blog/changelog/2022-05-1
|
||||
<summary>:eyes: Show example</summary>
|
||||
|
||||
<picture>
|
||||
<source
|
||||
<source
|
||||
srcset="https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
@@ -258,13 +260,13 @@ You can customize the appearance of your `Stats Card` or `Repo Card` however you
|
||||
- `border_color` - Card's border color _(hex color)_. Default: `e4e2e2` (Does not apply when `hide_border` is enabled).
|
||||
- `bg_color` - Card's background color _(hex color)_ **or** a gradient in the form of _angle,start,end_. Default: `fffefe`
|
||||
- `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.
|
||||
- `cache_seconds` - set the cache header manually _(min: 7200, max: 86400)_. Default: `14400 seconds (4 hours)`.
|
||||
- `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)`.
|
||||
- `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`.
|
||||
|
||||
> **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.
|
||||
> 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
|
||||
|
||||
@@ -278,6 +280,7 @@ You can provide multiple comma-separated values in the bg_color option to render
|
||||
- `hide_title` - _(boolean)_. Default: `false`.
|
||||
- `card_width` - Set the card's width manually _(number)_. Default: `500px (approx.)`.
|
||||
- `hide_rank` - _(boolean)_ hides the rank and automatically resizes the card width. Default: `false`.
|
||||
- `rank_icon` - Shows alternative rank icon (i.e. `github` or `default`). Default: `default`.
|
||||
- `show_icons` - _(boolean)_. Default: `false`.
|
||||
- `include_all_commits` - Count total commits instead of just the current year commits _(boolean)_. Default: `false`.
|
||||
- `count_private` - Count private commits _(boolean)_. Default: `false`.
|
||||
@@ -287,6 +290,7 @@ You can provide multiple comma-separated values in the bg_color option to render
|
||||
- `text_bold` - Use bold text _(boolean)_. Default: `true`.
|
||||
- `disable_animations` - Disables all animations in the card _(boolean)_. Default: `false`.
|
||||
- `ring_color` - Color of the rank circle _(hex color)_. Defaults to the theme ring color if it exists and otherwise the title color.
|
||||
- `number_format` - Switch between two available formats for displaying the card values `short` (i.e. `6.6k`) and `long` (i.e. `6626`). Default: `short`.
|
||||
|
||||
> **Note**
|
||||
> When hide_rank=`true`, the minimum card width is 270 px + the title length and padding.
|
||||
@@ -304,6 +308,10 @@ 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`.
|
||||
- `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`.
|
||||
- `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`.
|
||||
- `size_weight` - Configures language stats algorithm _(number)_ (see [Language stats algorithm](#Language-stats-algorithm)), defaults to 1.
|
||||
- `count_weight` - Configures language stats algorithm _(number)_ (see [Language stats algorithm](#Language-stats-algorithm)), defaults to 0.
|
||||
|
||||
> **Warning**
|
||||
> Language names should be URI-escaped, as specified in [Percent Encoding](https://en.wikipedia.org/wiki/Percent-encoding)
|
||||
@@ -353,7 +361,7 @@ Use [show_owner](#customization) variable to include the repo's owner username
|
||||
The top languages card shows a GitHub user's most frequently used top language.
|
||||
|
||||
> **Note**
|
||||
> Top Languages does not indicate my skill level or anything like that; it's a GitHub metric to determine which languages have the most code on GitHub. It is a new feature of github-readme-stats._
|
||||
> Top Languages does not indicate the user's 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
|
||||
|
||||
@@ -365,6 +373,24 @@ Endpoint: `api/top-langs?username=anuraghazra`
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Language stats algorithm
|
||||
|
||||
We use the following algorithm to calculate the languages percentages on the language card:
|
||||
|
||||
```js
|
||||
ranking_index = (byte_count ^ size_weight) * (repo_count ^ count_weight)
|
||||
```
|
||||
|
||||
By default, only the byte count is used for determining the languages percentages shown on the language card (i.e. `size_weight=1` and `count_weight=0`). You can, however, use the `&size_weight=` and `&count_weight=` options to weight the language usage calculation. The values must be positive real numbers. [More details about the algorithm can be found here](https://github.com/anuraghazra/github-readme-stats/issues/1600#issuecomment-1046056305).
|
||||
|
||||
- `&size_weight=1&count_weight=0` - _(default)_ Orders by byte count.
|
||||
- `&size_weight=0.5&count_weight=0.5` - _(recommended)_ Uses both byte and repo count for ranking
|
||||
- `&size_weight=0&count_weight=1` - Orders by repo count
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Exclude individual repositories
|
||||
|
||||
You can use the `&exclude_repo=repo1,repo2` parameter to exclude individual repositories.
|
||||
@@ -397,6 +423,14 @@ You can use the `&layout=compact` option to change the card design.
|
||||
[](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
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Demo
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
@@ -405,12 +439,16 @@ You can use the `&layout=compact` option to change the card design.
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
- Hidden progress bars
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
# Wakatime Week Stats
|
||||
|
||||
Change the `?username=` value to your [Wakatime](https://wakatime.com) username.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
> **Note**:
|
||||
@@ -418,13 +456,13 @@ Change the `?username=` value to your [Wakatime](https://wakatime.com) username.
|
||||
|
||||
### Demo
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
- Compact layout
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
* * *
|
||||
|
||||
@@ -442,6 +480,10 @@ Change the `?username=` value to your [Wakatime](https://wakatime.com) username.
|
||||
|
||||

|
||||
|
||||
- Shows Github logo instead rank level
|
||||
|
||||

|
||||
|
||||
- Customize Border Color
|
||||
|
||||

|
||||
@@ -478,7 +520,7 @@ Choose from any of the [default themes](#themes)
|
||||
|
||||
- WakaTime card
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
* * *
|
||||
|
||||
@@ -495,9 +537,11 @@ By default, GitHub does not lay out the cards side by side. To do that, you can
|
||||
</a>
|
||||
```
|
||||
|
||||
## Deploy on your own Vercel instance
|
||||
## Deploy on your own
|
||||
|
||||
#### [Check Out Step By Step Video Tutorial By @codeSTACKr](https://youtu.be/n6d4KHSKqGk?t=107)
|
||||
### On Vercel
|
||||
|
||||
#### :film_projector: [Check Out Step By Step Video Tutorial By @codeSTACKr](https://youtu.be/n6d4KHSKqGk?t=107)
|
||||
|
||||
> **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).
|
||||
@@ -532,6 +576,23 @@ Since the GitHub API only allows 5k requests per hour, my `https://github-readme
|
||||
|
||||
</details>
|
||||
|
||||
### On other platforms
|
||||
|
||||
> **Warning**
|
||||
> This way of using GRS is not officially supported and was added to cater to some particular use cases where Vercel could not be used (e.g. #2341). The support for this method, therefore, is limited.
|
||||
|
||||
<details>
|
||||
<summary><b>:hammer_and_wrench: Step-by-step guide for deploying on other platforms</b></summary>
|
||||
|
||||
1. Fork or clone this repo as per your needs
|
||||
2. Add `express` to the dependencies section of `package.json`
|
||||
<https://github.com/anuraghazra/github-readme-stats/blob/ba7c2f8b55eac8452e479c8bd38b044d204d0424/package.json#L54-L61>
|
||||
3. Run `npm i` if needed (initial setup)
|
||||
4. Run `node express.js` to start the server, or set the entry point to `express.js` in `package.json` if you're deploying on a managed service
|
||||
<https://github.com/anuraghazra/github-readme-stats/blob/ba7c2f8b55eac8452e479c8bd38b044d204d0424/package.json#L11>
|
||||
5. You're done 🎉
|
||||
</details>
|
||||
|
||||
### Keep your fork up to date
|
||||
|
||||
You can keep your fork, and thus your private Vercel instance up to date with the upstream using GitHubs' [Sync Fork button](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork). You can also use the [pull](https://github.com/wei/pull) package created by [@wei](https://github.com/wei) to automate this process.
|
||||
|
||||
+131
-63
@@ -26,9 +26,9 @@ const FAIL_TEXT = `
|
||||
\rUnfortunately, your theme PR contains an error or does not adhere to our [theme guidelines](https://github.com/anuraghazra/github-readme-stats/blob/master/CONTRIBUTING.md#themes-contribution). Please fix the issues below, and we will review your\
|
||||
\r PR again. This pull request will **automatically close in 20 days** if no changes are made. After this time, you must re-open the PR for it to be reviewed.
|
||||
`;
|
||||
const THEME_CONTRIB_GUIDELINESS = `
|
||||
const THEME_CONTRIB_GUIDELINES = `
|
||||
\rHi, thanks for the theme contribution. Please read our theme [contribution guidelines](https://github.com/anuraghazra/github-readme-stats/blob/master/CONTRIBUTING.md#themes-contribution).
|
||||
\rWe are currently only accepting color combinations from any VSCode theme or themes with good colour combinations to minimize bloating the themes collection.
|
||||
\rWe are currently only accepting color combinations from any VSCode theme or themes with good color combinations to minimize bloating the themes collection.
|
||||
|
||||
\r> Also, note that if this theme is exclusively for your personal use, then instead of adding it to our theme collection, you can use card [customization options](https://github.com/anuraghazra/github-readme-stats#customization).
|
||||
`;
|
||||
@@ -43,6 +43,23 @@ const ACCEPTED_COLOR_PROPS = Object.keys(COLOR_PROPS);
|
||||
const REQUIRED_COLOR_PROPS = ACCEPTED_COLOR_PROPS.slice(0, 4);
|
||||
const INVALID_REVIEW_COMMENT = (commentUrl) =>
|
||||
`Some themes are invalid. See the [Automated Theme Preview](${commentUrl}) comment above for more information.`;
|
||||
var OCTOKIT;
|
||||
var OWNER;
|
||||
var REPO;
|
||||
var PULL_REQUEST_ID;
|
||||
|
||||
/**
|
||||
* Incorrect JSON format error.
|
||||
* @extends Error
|
||||
* @param {string} message Error message.
|
||||
* @returns {Error} IncorrectJsonFormatError.
|
||||
*/
|
||||
class IncorrectJsonFormatError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "IncorrectJsonFormatError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve PR number from the event payload.
|
||||
@@ -126,15 +143,36 @@ const findComment = async (octokit, issueNumber, owner, repo, commenter) => {
|
||||
* Create or update the preview comment.
|
||||
*
|
||||
* @param {Object} octokit Octokit instance.
|
||||
* @param {Object} props Comment properties.
|
||||
* @param {number} issueNumber Issue number.
|
||||
* @param {Object} repo Repository name.
|
||||
* @param {Object} owner Owner of the repository.
|
||||
* @param {number} commentId Comment ID.
|
||||
* @param {string} body Comment body.
|
||||
* @return {string} The comment URL.
|
||||
*/
|
||||
const upsertComment = async (octokit, props) => {
|
||||
const upsertComment = async (
|
||||
octokit,
|
||||
issueNumber,
|
||||
repo,
|
||||
owner,
|
||||
commentId,
|
||||
body,
|
||||
) => {
|
||||
let resp;
|
||||
if (props.comment_id !== undefined) {
|
||||
resp = await octokit.issues.updateComment(props);
|
||||
if (commentId !== undefined) {
|
||||
resp = await octokit.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: commentId,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
resp = await octokit.issues.createComment(props);
|
||||
resp = await octokit.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
body,
|
||||
});
|
||||
}
|
||||
return resp.data.html_url;
|
||||
};
|
||||
@@ -269,22 +307,38 @@ const parseJSON = (json) => {
|
||||
if (typeof parsedJson === "object") {
|
||||
return parsedJson;
|
||||
} else {
|
||||
throw new Error("PR diff is not a valid theme JSON object.");
|
||||
throw new IncorrectJsonFormatError(
|
||||
"PR diff is not a valid theme JSON object.",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
let parsedJson = json
|
||||
// Remove trailing commas (if any).
|
||||
let parsedJson = json.replace(/(,\s*})/g, "}");
|
||||
|
||||
// Remove JS comments (if any).
|
||||
parsedJson = parsedJson.replace(/\/\/[A-z\s]*\s/g, "");
|
||||
|
||||
// Fix incorrect open bracket (if any).
|
||||
const splitJson = parsedJson
|
||||
.split(/([\s\r\s]*}[\s\r\s]*,[\s\r\s]*)(?=[\w"-]+:)/)
|
||||
.filter((x) => typeof x !== "string" || !!x.trim());
|
||||
if (parsedJson[0].replace(/\s+/g, "") === "},") {
|
||||
parsedJson[0] = "},";
|
||||
if (!/\s*}\s*,?\s*$/.test(parsedJson[1])) {
|
||||
parsedJson.push(parsedJson.shift());
|
||||
.filter((x) => typeof x !== "string" || !!x.trim()); // Split json into array of strings and objects.
|
||||
if (splitJson[0].replace(/\s+/g, "") === "},") {
|
||||
splitJson[0] = "},";
|
||||
if (!/\s*}\s*,?\s*$/.test(splitJson[1])) {
|
||||
splitJson.push(splitJson.shift());
|
||||
} else {
|
||||
parsedJson.shift();
|
||||
splitJson.shift();
|
||||
}
|
||||
return Hjson.parse(parsedJson.join(""));
|
||||
} else {
|
||||
throw error;
|
||||
parsedJson = splitJson.join("");
|
||||
}
|
||||
|
||||
// Try to parse the fixed json.
|
||||
try {
|
||||
return Hjson.parse(parsedJson);
|
||||
} catch (error) {
|
||||
throw new IncorrectJsonFormatError(
|
||||
`Theme JSON file could not be parsed: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -298,53 +352,64 @@ const themeNameAlreadyExists = (name) => {
|
||||
return themes[name] !== undefined;
|
||||
};
|
||||
|
||||
const DRY_RUN = process.env.DRY_RUN === "true" || false;
|
||||
|
||||
/**
|
||||
* Main function.
|
||||
*/
|
||||
export const run = async (prNumber) => {
|
||||
export const run = async () => {
|
||||
try {
|
||||
const dryRun = process.env.DRY_RUN === "true" || false;
|
||||
debug("Retrieve action information from context...");
|
||||
debug(`Context: ${inspect(github.context)}`);
|
||||
let commentBody = `
|
||||
\r# ${COMMENT_TITLE}
|
||||
\r${THEME_CONTRIB_GUIDELINESS}
|
||||
\r${THEME_CONTRIB_GUIDELINES}
|
||||
`;
|
||||
const ccc = new ColorContrastChecker();
|
||||
const octokit = github.getOctokit(getGithubToken());
|
||||
const pullRequestId = prNumber ? prNumber : getPrNumber();
|
||||
const commenter = getCommenter();
|
||||
OCTOKIT = github.getOctokit(getGithubToken());
|
||||
PULL_REQUEST_ID = getPrNumber();
|
||||
const { owner, repo } = getRepoInfo(github.context);
|
||||
debug(`Owner: ${owner}`);
|
||||
debug(`Repo: ${repo}`);
|
||||
OWNER = owner;
|
||||
REPO = repo;
|
||||
const commenter = getCommenter();
|
||||
PULL_REQUEST_ID = getPrNumber();
|
||||
debug(`Owner: ${OWNER}`);
|
||||
debug(`Repo: ${REPO}`);
|
||||
debug(`Commenter: ${commenter}`);
|
||||
|
||||
// Retrieve the PR diff and preview-theme comment.
|
||||
debug("Retrieve PR diff...");
|
||||
const res = await octokit.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pullRequestId,
|
||||
const res = await OCTOKIT.pulls.get({
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
pull_number: PULL_REQUEST_ID,
|
||||
mediaType: {
|
||||
format: "diff",
|
||||
},
|
||||
});
|
||||
debug("Retrieve preview-theme comment...");
|
||||
const comment = await findComment(
|
||||
octokit,
|
||||
pullRequestId,
|
||||
owner,
|
||||
repo,
|
||||
OCTOKIT,
|
||||
PULL_REQUEST_ID,
|
||||
OWNER,
|
||||
REPO,
|
||||
commenter,
|
||||
);
|
||||
|
||||
// Retrieve theme changes from the PR diff.
|
||||
debug("Retrieve themes...");
|
||||
const diff = parse(res.data);
|
||||
|
||||
// Retrieve all theme changes from the PR diff and convert to JSON.
|
||||
debug("Retrieve theme changes...");
|
||||
const content = diff
|
||||
.find((file) => file.to === "themes/index.js")
|
||||
.chunks[0].changes.filter((c) => c.type === "add")
|
||||
.map((c) => c.content.replace("+", ""))
|
||||
.chunks.map((chunk) =>
|
||||
chunk.changes
|
||||
.filter((c) => c.type === "add")
|
||||
.map((c) => c.content.replace("+", ""))
|
||||
.join(""),
|
||||
)
|
||||
.join("");
|
||||
const themeObject = parseJSON(content);
|
||||
if (
|
||||
@@ -513,14 +578,15 @@ export const run = async (prNumber) => {
|
||||
// Create or update theme-preview comment.
|
||||
debug("Create or update theme-preview comment...");
|
||||
let comment_url;
|
||||
if (!dryRun) {
|
||||
comment_url = await upsertComment(octokit, {
|
||||
comment_id: comment?.id,
|
||||
issue_number: pullRequestId,
|
||||
owner,
|
||||
repo,
|
||||
body: commentBody,
|
||||
});
|
||||
if (!DRY_RUN) {
|
||||
comment_url = await upsertComment(
|
||||
OCTOKIT,
|
||||
PULL_REQUEST_ID,
|
||||
REPO,
|
||||
OWNER,
|
||||
comment?.id,
|
||||
commentBody,
|
||||
);
|
||||
} else {
|
||||
info(`DRY_RUN: Comment body: ${commentBody}`);
|
||||
comment_url = "";
|
||||
@@ -535,20 +601,20 @@ export const run = async (prNumber) => {
|
||||
const reviewReason = themesValid
|
||||
? undefined
|
||||
: INVALID_REVIEW_COMMENT(comment_url);
|
||||
if (!dryRun) {
|
||||
if (!DRY_RUN) {
|
||||
await addReview(
|
||||
octokit,
|
||||
pullRequestId,
|
||||
owner,
|
||||
repo,
|
||||
OCTOKIT,
|
||||
PULL_REQUEST_ID,
|
||||
OWNER,
|
||||
REPO,
|
||||
reviewState,
|
||||
reviewReason,
|
||||
);
|
||||
await addRemoveLabel(
|
||||
octokit,
|
||||
pullRequestId,
|
||||
owner,
|
||||
repo,
|
||||
OCTOKIT,
|
||||
PULL_REQUEST_ID,
|
||||
OWNER,
|
||||
REPO,
|
||||
"invalid",
|
||||
!themesValid,
|
||||
);
|
||||
@@ -558,20 +624,22 @@ export const run = async (prNumber) => {
|
||||
}
|
||||
} catch (error) {
|
||||
debug("Set review state to `REQUEST_CHANGES` and add `invalid` label...");
|
||||
if (!dryRun) {
|
||||
if (!DRY_RUN) {
|
||||
await addReview(
|
||||
octokit,
|
||||
pullRequestId,
|
||||
owner,
|
||||
repo,
|
||||
OCTOKIT,
|
||||
PULL_REQUEST_ID,
|
||||
OWNER,
|
||||
REPO,
|
||||
"REQUEST_CHANGES",
|
||||
error.message,
|
||||
"**Something went wrong in the theme preview action:** `" +
|
||||
error.message +
|
||||
"`",
|
||||
);
|
||||
await addRemoveLabel(
|
||||
octokit,
|
||||
pullRequestId,
|
||||
owner,
|
||||
repo,
|
||||
OCTOKIT,
|
||||
PULL_REQUEST_ID,
|
||||
OWNER,
|
||||
REPO,
|
||||
"invalid",
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -6,9 +6,10 @@ export BRANCH_NAME=updated-theme-readme
|
||||
git --version
|
||||
git config --global user.email "no-reply@githubreadmestats.com"
|
||||
git config --global user.name "GitHub Readme Stats Bot"
|
||||
git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||
git branch -d $BRANCH_NAME || true
|
||||
git checkout -b $BRANCH_NAME
|
||||
git add --all
|
||||
git commit --message "docs(theme): Auto update theme readme" || exit 0
|
||||
git commit --no-verify --message "docs(theme): Auto update theme readme"
|
||||
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
|
||||
|
||||
@@ -29,13 +29,14 @@ const normalcdf = (mean, sigma, to) => {
|
||||
/**
|
||||
* Calculates the users rank.
|
||||
*
|
||||
* @param {number} totalRepos Total number of repos.
|
||||
* @param {number} totalCommits Total number of commits.
|
||||
* @param {number} contributions The number of contributions.
|
||||
* @param {number} followers The number of followers.
|
||||
* @param {number} prs The number of pull requests.
|
||||
* @param {number} issues The number of issues.
|
||||
* @param {number} stargazers The number of stars.
|
||||
* @param {object} params Parameters on which the user's rank depends.
|
||||
* @param {number} params.totalRepos Total number of repos.
|
||||
* @param {number} params.totalCommits Total number of commits.
|
||||
* @param {number} params.contributions The number of contributions.
|
||||
* @param {number} params.followers The number of followers.
|
||||
* @param {number} params.prs The number of pull requests.
|
||||
* @param {number} params.issues The number of issues.
|
||||
* @param {number} params.stargazers The number of stars.
|
||||
* @returns {{level: string, score: number}}} The users rank.
|
||||
*/
|
||||
const calculateRank = ({
|
||||
|
||||
+38
-25
@@ -1,7 +1,7 @@
|
||||
// @ts-check
|
||||
import { Card } from "../common/Card.js";
|
||||
import { I18n } from "../common/I18n.js";
|
||||
import { icons } from "../common/icons.js";
|
||||
import { icons, rankIcon } from "../common/icons.js";
|
||||
import {
|
||||
clampValue,
|
||||
flexLayout,
|
||||
@@ -12,17 +12,24 @@ import {
|
||||
import { getStyles } from "../getStyles.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.
|
||||
*
|
||||
* @param {object[]} createTextNodeParams Object that contains the createTextNode parameters.
|
||||
* @param {object} createTextNodeParams Object that contains the createTextNode parameters.
|
||||
* @param {string} createTextNodeParams.icon The icon to display.
|
||||
* @param {string} createTextNodeParams.label The label to display.
|
||||
* @param {string} createTextNodeParams.value The value to display.
|
||||
* @param {number} createTextNodeParams.value The value to display.
|
||||
* @param {string} createTextNodeParams.id The id of the stat.
|
||||
* @param {number} createTextNodeParams.index The index of the stat.
|
||||
* @param {boolean} createTextNodeParams.showIcons Whether to show icons.
|
||||
* @param {number} createTextNodeParams.shiftValuePos Number of pixels the value has to be shifted to the right.
|
||||
* @param {boolean} createTextNodeParams.bold Whether to bold the label.
|
||||
* @param {string} createTextNodeParams.number_format The format of numbers on card.
|
||||
* @returns
|
||||
*/
|
||||
const createTextNode = ({
|
||||
@@ -34,8 +41,10 @@ const createTextNode = ({
|
||||
showIcons,
|
||||
shiftValuePos,
|
||||
bold,
|
||||
number_format,
|
||||
}) => {
|
||||
const kValue = kFormatter(value);
|
||||
const kValue =
|
||||
number_format.toLowerCase() === "long" ? value : kFormatter(value);
|
||||
const staggerDelay = (index + 3) * 150;
|
||||
|
||||
const labelOffset = showIcons ? `x="25"` : "";
|
||||
@@ -98,8 +107,10 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
custom_title,
|
||||
border_radius,
|
||||
border_color,
|
||||
number_format = "short",
|
||||
locale,
|
||||
disable_animations = false,
|
||||
rank_icon = "default",
|
||||
} = options;
|
||||
|
||||
const lheight = parseInt(String(line_height), 10);
|
||||
@@ -174,7 +185,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
"nl",
|
||||
"zh-tw",
|
||||
];
|
||||
const isLongLocale = longLocales.includes(locale) === true;
|
||||
const isLongLocale = longLocales.includes(locale);
|
||||
|
||||
// filter out hidden stats defined by user & create the text nodes
|
||||
const statItems = Object.keys(STATS)
|
||||
@@ -187,6 +198,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
showIcons: show_icons,
|
||||
shiftValuePos: 79.01 + (isLongLocale ? 50 : 0),
|
||||
bold: text_bold,
|
||||
number_format,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -218,11 +230,17 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
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.
|
||||
*/
|
||||
const iconWidth = show_icons ? 16 : 0;
|
||||
const minCardWidth = hide_rank
|
||||
? clampValue(50 /* padding */ + calculateTextWidth() * 2, 270, Infinity)
|
||||
: 340 + iconWidth;
|
||||
const defaultCardWidth = hide_rank ? 270 : 495;
|
||||
const iconWidth = show_icons ? 16 + /* padding */ 1 : 0;
|
||||
const minCardWidth =
|
||||
(hide_rank
|
||||
? clampValue(
|
||||
50 /* padding */ + calculateTextWidth() * 2,
|
||||
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;
|
||||
if (width < minCardWidth) {
|
||||
width = minCardWidth;
|
||||
@@ -251,18 +269,21 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
|
||||
/**
|
||||
* Calculates the right rank circle translation values such that the rank circle
|
||||
* keeps respecting the padding.
|
||||
* keeps respecting the following padding:
|
||||
*
|
||||
* width > 450: The default left padding of 50 px will be used.
|
||||
* width < 450: The left and right padding will shrink equally.
|
||||
* width > RANK_CARD_DEFAULT_WIDTH: The default right padding of 70 px will be used.
|
||||
* width < RANK_CARD_DEFAULT_WIDTH: The left and right padding will be enlarged
|
||||
* equally from a certain minimum at RANK_CARD_MIN_WIDTH.
|
||||
*
|
||||
* @returns {number} - Rank circle translation value.
|
||||
*/
|
||||
const calculateRankXTranslation = () => {
|
||||
if (width < 450) {
|
||||
return width - 95 + (45 * (450 - 340)) / 110;
|
||||
const minXTranslation = RANK_CARD_MIN_WIDTH + iconWidth - 70;
|
||||
if (width > RANK_CARD_DEFAULT_WIDTH) {
|
||||
const xMaxExpansion = minXTranslation + (450 - minCardWidth) / 2;
|
||||
return xMaxExpansion + width - RANK_CARD_DEFAULT_WIDTH;
|
||||
} else {
|
||||
return width - 95;
|
||||
return minXTranslation + (width - minCardWidth) / 2;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -276,15 +297,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
<circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
|
||||
<circle class="rank-circle" cx="-10" cy="8" r="40" />
|
||||
<g class="rank-text">
|
||||
<text
|
||||
x="-5"
|
||||
y="3"
|
||||
alignment-baseline="central"
|
||||
dominant-baseline="central"
|
||||
text-anchor="middle"
|
||||
>
|
||||
${rank.level}
|
||||
</text>
|
||||
${rankIcon(rank_icon, rank?.level)}
|
||||
</g>
|
||||
</g>`;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { langCardLocales } from "../translations.js";
|
||||
|
||||
const DEFAULT_CARD_WIDTH = 300;
|
||||
const MIN_CARD_WIDTH = 230;
|
||||
const MIN_CARD_WIDTH = 280;
|
||||
const DEFAULT_LANGS_COUNT = 5;
|
||||
const DEFAULT_LANG_COLOR = "#858585";
|
||||
const CARD_PADDING = 25;
|
||||
@@ -39,49 +39,57 @@ const getLongestLang = (arr) =>
|
||||
* Creates a node to display usage of a programming language in percentage
|
||||
* 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 {string} props.name Name 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 {number} props.index Index of the programming language.
|
||||
* @returns {string} Programming language SVG node.
|
||||
*/
|
||||
const createProgressTextNode = ({ width, color, name, progress }) => {
|
||||
const createProgressTextNode = ({ width, color, name, progress, index }) => {
|
||||
const staggerDelay = (index + 3) * 150;
|
||||
const paddingRight = 95;
|
||||
const progressTextX = width - paddingRight + 10;
|
||||
const progressWidth = width - paddingRight;
|
||||
|
||||
return `
|
||||
<text data-testid="lang-name" x="2" y="15" class="lang-name">${name}</text>
|
||||
<text x="${progressTextX}" y="34" class="lang-name">${progress}%</text>
|
||||
${createProgressNode({
|
||||
x: 0,
|
||||
y: 25,
|
||||
color,
|
||||
width: progressWidth,
|
||||
progress,
|
||||
progressBarBackgroundColor: "#ddd",
|
||||
})}
|
||||
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
|
||||
<text data-testid="lang-name" x="2" y="15" class="lang-name">${name}</text>
|
||||
<text x="${progressTextX}" y="34" class="lang-name">${progress}%</text>
|
||||
${createProgressNode({
|
||||
x: 0,
|
||||
y: 25,
|
||||
color,
|
||||
width: progressWidth,
|
||||
progress,
|
||||
progressBarBackgroundColor: "#ddd",
|
||||
delay: staggerDelay + 300,
|
||||
})}
|
||||
</g>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 {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.
|
||||
*/
|
||||
const createCompactLangNode = ({ lang, totalSize }) => {
|
||||
const createCompactLangNode = ({ lang, totalSize, hideProgress, index }) => {
|
||||
const percentage = ((lang.size / totalSize) * 100).toFixed(2);
|
||||
const staggerDelay = (index + 3) * 150;
|
||||
const color = lang.color || "#858585";
|
||||
|
||||
return `
|
||||
<g>
|
||||
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
|
||||
<circle cx="5" cy="6" r="5" fill="${color}" />
|
||||
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
||||
${lang.name} ${percentage}%
|
||||
${lang.name} ${hideProgress ? "" : percentage + "%"}
|
||||
</text>
|
||||
</g>
|
||||
`;
|
||||
@@ -93,9 +101,10 @@ const createCompactLangNode = ({ lang, totalSize }) => {
|
||||
* @param {object[]} props Function properties.
|
||||
* @param {Lang[]} props.langs Array of programming languages.
|
||||
* @param {number} props.totalSize Total size of all languages.
|
||||
* @param {boolean} props.hideProgress Whether to hide percentage.
|
||||
* @returns {string} Programming languages SVG node.
|
||||
*/
|
||||
const createLanguageTextNode = ({ langs, totalSize }) => {
|
||||
const createLanguageTextNode = ({ langs, totalSize, hideProgress }) => {
|
||||
const longestLang = getLongestLang(langs);
|
||||
const chunked = chunkArray(langs, langs.length / 2);
|
||||
const layouts = chunked.map((array) => {
|
||||
@@ -104,7 +113,7 @@ const createLanguageTextNode = ({ langs, totalSize }) => {
|
||||
createCompactLangNode({
|
||||
lang,
|
||||
totalSize,
|
||||
// @ts-ignore
|
||||
hideProgress,
|
||||
index,
|
||||
}),
|
||||
);
|
||||
@@ -134,12 +143,13 @@ const createLanguageTextNode = ({ langs, totalSize }) => {
|
||||
*/
|
||||
const renderNormalLayout = (langs, width, totalLanguageSize) => {
|
||||
return flexLayout({
|
||||
items: langs.map((lang) => {
|
||||
items: langs.map((lang, index) => {
|
||||
return createProgressTextNode({
|
||||
width: width,
|
||||
width,
|
||||
name: lang.name,
|
||||
color: lang.color || DEFAULT_LANG_COLOR,
|
||||
progress: ((lang.size / totalLanguageSize) * 100).toFixed(2),
|
||||
index,
|
||||
});
|
||||
}),
|
||||
gap: 40,
|
||||
@@ -153,9 +163,10 @@ const renderNormalLayout = (langs, width, totalLanguageSize) => {
|
||||
* @param {Lang[]} langs Array of programming languages.
|
||||
* @param {number} width Card width.
|
||||
* @param {number} totalLanguageSize Total size of all languages.
|
||||
* @param {boolean} hideProgress Whether to hide progress bar.
|
||||
* @returns {string} Compact layout card SVG object.
|
||||
*/
|
||||
const renderCompactLayout = (langs, width, totalLanguageSize) => {
|
||||
const renderCompactLayout = (langs, width, totalLanguageSize, hideProgress) => {
|
||||
const paddingRight = 50;
|
||||
const offsetWidth = width - paddingRight;
|
||||
// progressOffset holds the previous language's width and used to offset the next language
|
||||
@@ -186,15 +197,21 @@ const renderCompactLayout = (langs, width, totalLanguageSize) => {
|
||||
.join("");
|
||||
|
||||
return `
|
||||
<mask id="rect-mask">
|
||||
<rect x="0" y="0" width="${offsetWidth}" height="8" fill="white" rx="5" />
|
||||
${
|
||||
!hideProgress
|
||||
? `
|
||||
<mask id="rect-mask">
|
||||
<rect x="0" y="0" width="${offsetWidth}" height="8" fill="white" rx="5"/>
|
||||
</mask>
|
||||
${compactProgressBar}
|
||||
|
||||
<g transform="translate(0, 25)">
|
||||
`
|
||||
: ""
|
||||
}
|
||||
<g transform="translate(0, ${hideProgress ? "0" : "25"})">
|
||||
${createLanguageTextNode({
|
||||
langs,
|
||||
totalSize: totalLanguageSize,
|
||||
hideProgress: hideProgress,
|
||||
})}
|
||||
</g>
|
||||
`;
|
||||
@@ -269,6 +286,7 @@ const renderTopLanguages = (topLangs, options = {}) => {
|
||||
text_color,
|
||||
bg_color,
|
||||
hide,
|
||||
hide_progress,
|
||||
theme,
|
||||
layout,
|
||||
custom_title,
|
||||
@@ -276,6 +294,7 @@ const renderTopLanguages = (topLangs, options = {}) => {
|
||||
langs_count = DEFAULT_LANGS_COUNT,
|
||||
border_radius,
|
||||
border_color,
|
||||
disable_animations,
|
||||
} = options;
|
||||
|
||||
const i18n = new I18n({
|
||||
@@ -297,11 +316,16 @@ const renderTopLanguages = (topLangs, options = {}) => {
|
||||
let height = calculateNormalLayoutHeight(langs.length);
|
||||
|
||||
let finalLayout = "";
|
||||
if (layout === "compact") {
|
||||
width = width + 50; // padding
|
||||
height = calculateCompactLayoutHeight(langs.length);
|
||||
if (layout === "compact" || hide_progress == true) {
|
||||
height =
|
||||
calculateCompactLayoutHeight(langs.length) + (hide_progress ? -25 : 0);
|
||||
|
||||
finalLayout = renderCompactLayout(langs, width, totalLanguageSize);
|
||||
finalLayout = renderCompactLayout(
|
||||
langs,
|
||||
width,
|
||||
totalLanguageSize,
|
||||
hide_progress,
|
||||
);
|
||||
} else {
|
||||
finalLayout = renderNormalLayout(langs, width, totalLanguageSize);
|
||||
}
|
||||
@@ -324,11 +348,43 @@ const renderTopLanguages = (topLangs, options = {}) => {
|
||||
colors,
|
||||
});
|
||||
|
||||
card.disableAnimations();
|
||||
if (disable_animations) card.disableAnimations();
|
||||
|
||||
card.setHideBorder(hide_border);
|
||||
card.setHideTitle(hide_title);
|
||||
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(`
|
||||
|
||||
Vendored
+7
@@ -1,4 +1,5 @@
|
||||
type ThemeNames = keyof typeof import("../../themes/index.js");
|
||||
type RankIcon = "default" | "github";
|
||||
|
||||
export type CommonOptions = {
|
||||
title_color: string;
|
||||
@@ -22,6 +23,10 @@ export type StatCardOptions = CommonOptions & {
|
||||
line_height: number | string;
|
||||
custom_title: string;
|
||||
disable_animations: boolean;
|
||||
number_format: string;
|
||||
ring_color: string;
|
||||
text_bold: boolean;
|
||||
rank_icon: RankIcon;
|
||||
};
|
||||
|
||||
export type RepoCardOptions = CommonOptions & {
|
||||
@@ -37,6 +42,8 @@ export type TopLangOptions = CommonOptions & {
|
||||
layout: "compact" | "normal";
|
||||
custom_title: string;
|
||||
langs_count: number;
|
||||
disable_animations: boolean;
|
||||
hide_progress: boolean;
|
||||
};
|
||||
|
||||
type WakaTimeOptions = CommonOptions & {
|
||||
|
||||
+58
-25
@@ -36,11 +36,11 @@ const noCodingActivityNode = ({ color, text }) => {
|
||||
/**
|
||||
* Create compact WakaTime layout.
|
||||
*
|
||||
* @param {Object[]} args The function arguments.
|
||||
* @param {import("../fetchers/types").WakaTimeLang[]} languages The languages array.
|
||||
* @param {number} totalSize The total size of the languages.
|
||||
* @param {number} x The x position of the language node.
|
||||
* @param {number} y The y position of the language node.
|
||||
* @param {Object} args The function arguments.
|
||||
* @param {import("../fetchers/types").WakaTimeLang} args.lang The languages array.
|
||||
* @param {number} args.totalSize The total size of the languages.
|
||||
* @param {number} args.x The x position of the language node.
|
||||
* @param {number} args.y The y position of the language node.
|
||||
*/
|
||||
const createCompactLangNode = ({ lang, totalSize, x, y }) => {
|
||||
const color = languageColors[lang.name] || "#858585";
|
||||
@@ -58,11 +58,11 @@ const createCompactLangNode = ({ lang, totalSize, x, y }) => {
|
||||
/**
|
||||
* Create WakaTime language text node item.
|
||||
*
|
||||
* @param {Object[]} args The function arguments.
|
||||
* @param {import("../fetchers/types").WakaTimeLang} lang The language object.
|
||||
* @param {number} totalSize The total size of the languages.
|
||||
* @param {number} x The x position of the language node.
|
||||
* @param {number} y The y position of the language node.
|
||||
* @param {Object} args The function arguments.
|
||||
* @param {import("../fetchers/types").WakaTimeLang[]} args.langs The language objects.
|
||||
* @param {number} args.totalSize The total size of the languages.
|
||||
* @param {number} args.x The x position of the language node.
|
||||
* @param {number} args.y The y position of the language node.
|
||||
*/
|
||||
const createLanguageTextNode = ({ langs, totalSize, x, y }) => {
|
||||
return langs.map((lang, index) => {
|
||||
@@ -86,14 +86,15 @@ const createLanguageTextNode = ({ langs, totalSize, x, y }) => {
|
||||
/**
|
||||
* Create WakaTime text item.
|
||||
*
|
||||
* @param {Object[]} args The function arguments.
|
||||
* @param {string} id The id of the text node item.
|
||||
* @param {string} label The label of the text node item.
|
||||
* @param {string} value The value of the text node item.
|
||||
* @param {number} index The index of the text node item.
|
||||
* @param {percent} percent Percentage of the text node item.
|
||||
* @param {boolean} hideProgress Whether to hide the progress bar.
|
||||
* @param {string} progressBarBackgroundColor The color of the progress bar background.
|
||||
* @param {Object} args The function arguments.
|
||||
* @param {string} args.id The id of the text node item.
|
||||
* @param {string} args.label The label of the text node item.
|
||||
* @param {string} args.value The value of the text node item.
|
||||
* @param {number} args.index The index of the text node item.
|
||||
* @param {string} args.percent Percentage of the text node item.
|
||||
* @param {boolean} args.hideProgress Whether to hide the progress bar.
|
||||
* @param {string} args.progressBarColor The color of the progress bar.
|
||||
* @param {string} args.progressBarBackgroundColor The color of the progress bar background.
|
||||
*/
|
||||
const createTextNode = ({
|
||||
id,
|
||||
@@ -118,6 +119,7 @@ const createTextNode = ({
|
||||
// @ts-ignore
|
||||
name: label,
|
||||
progressBarBackgroundColor,
|
||||
delay: staggerDelay + 300,
|
||||
});
|
||||
|
||||
return `
|
||||
@@ -266,21 +268,30 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
|
||||
<rect x="25" y="0" width="${width - 50}" height="8" fill="white" rx="5" />
|
||||
</mask>
|
||||
${compactProgressBar}
|
||||
${createLanguageTextNode({
|
||||
x: 0,
|
||||
y: 25,
|
||||
langs: filteredLanguages,
|
||||
totalSize: 100,
|
||||
}).join("")}
|
||||
${
|
||||
filteredLanguages.length
|
||||
? createLanguageTextNode({
|
||||
x: 0,
|
||||
y: 25,
|
||||
langs: filteredLanguages,
|
||||
totalSize: 100,
|
||||
}).join("")
|
||||
: noCodingActivityNode({
|
||||
// @ts-ignore
|
||||
color: textColor,
|
||||
text: i18n.t("wakatimecard.nocodingactivity"),
|
||||
})
|
||||
}
|
||||
`;
|
||||
} else {
|
||||
finalLayout = flexLayout({
|
||||
items: filteredLanguages.length
|
||||
? filteredLanguages.map((language) => {
|
||||
? filteredLanguages.map((language, index) => {
|
||||
return createTextNode({
|
||||
id: language.name,
|
||||
label: language.name,
|
||||
value: language.text,
|
||||
index: index,
|
||||
percent: language.percent,
|
||||
// @ts-ignore
|
||||
progressBarColor: titleColor,
|
||||
@@ -321,7 +332,29 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
|
||||
card.setCSS(
|
||||
`
|
||||
${cssStyles}
|
||||
@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: ${textColor} }
|
||||
#rect-mask rect{
|
||||
animation: slideInAnimation 1s ease-in-out forwards;
|
||||
}
|
||||
.lang-progress{
|
||||
animation: growWidthAnimation 0.6s ease-in-out forwards;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
|
||||
@@ -12,6 +12,12 @@ class Card {
|
||||
* @param {string?=} args.customTitle Card custom title.
|
||||
* @param {string?=} args.defaultTitle Card default title.
|
||||
* @param {string?=} args.titlePrefixIcon Card title prefix icon.
|
||||
* @param {object?=} args.colors Card colors arguments.
|
||||
* @param {string} args.colors.titleColor Card title color.
|
||||
* @param {string} args.colors.textColor Card text color.
|
||||
* @param {string} args.colors.iconColor Card icon color.
|
||||
* @param {string|Array} args.colors.bgColor Card background color.
|
||||
* @param {string} args.colors.borderColor Card border color.
|
||||
* @returns {Card} Card instance.
|
||||
*/
|
||||
constructor({
|
||||
|
||||
@@ -10,6 +10,7 @@ import { clampValue } from "./utils.js";
|
||||
* @param {string} createProgressNodeParams.color Progress color.
|
||||
* @param {string} createProgressNodeParams.progress Progress value.
|
||||
* @param {string} createProgressNodeParams.progressBarBackgroundColor Progress bar bg color.
|
||||
* @param {number} createProgressNodeParams.delay Delay before animation starts.
|
||||
* @returns {string} Progress node.
|
||||
*/
|
||||
const createProgressNode = ({
|
||||
@@ -19,20 +20,22 @@ const createProgressNode = ({
|
||||
color,
|
||||
progress,
|
||||
progressBarBackgroundColor,
|
||||
delay,
|
||||
}) => {
|
||||
const progressPercentage = clampValue(progress, 2, 100);
|
||||
|
||||
return `
|
||||
<svg width="${width}" x="${x}" y="${y}">
|
||||
<rect rx="5" ry="5" x="0" y="0" width="${width}" height="8" fill="${progressBarBackgroundColor}"></rect>
|
||||
<rect
|
||||
height="8"
|
||||
fill="${color}"
|
||||
rx="5" ry="5" x="0" y="0"
|
||||
data-testid="lang-progress"
|
||||
width="${progressPercentage}%"
|
||||
>
|
||||
</rect>
|
||||
<svg data-testid="lang-progress" width="${progressPercentage}%">
|
||||
<rect
|
||||
height="8"
|
||||
fill="${color}"
|
||||
rx="5" ry="5" x="0" y="0"
|
||||
class="lang-progress"
|
||||
style="animation-delay: ${delay}ms;"
|
||||
/>
|
||||
</svg>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
|
||||
+24
-1
@@ -8,5 +8,28 @@ const icons = {
|
||||
fork: `<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path>`,
|
||||
};
|
||||
|
||||
export { icons };
|
||||
/**
|
||||
* Get rank icon
|
||||
*
|
||||
* @returns {string} - The SVG code of the rank icon
|
||||
*/
|
||||
const rankIcon = (rankIcon, rankLevel) => {
|
||||
switch (rankIcon) {
|
||||
case "github":
|
||||
return `
|
||||
<svg x="-38" y="-30" height="66" width="66" aria-hidden="true" viewBox="0 0 16 16" version="1.1" data-view-component="true" data-testid="github-rank-icon">
|
||||
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
|
||||
</svg>
|
||||
`;
|
||||
case "default":
|
||||
default:
|
||||
return `
|
||||
<text x="-5" y="3" alignment-baseline="central" dominant-baseline="central" text-anchor="middle" data-testid="level-rank-icon">
|
||||
${rankLevel}
|
||||
</text>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
export { icons, rankIcon };
|
||||
export default icons;
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
"Ceylon": "#dfa535",
|
||||
"Chapel": "#8dc63f",
|
||||
"ChucK": "#3f8000",
|
||||
"Circom": "#707575",
|
||||
"Cirru": "#ccccff",
|
||||
"Clarion": "#db901e",
|
||||
"Clarity": "#5546ff",
|
||||
@@ -102,6 +103,7 @@
|
||||
"Csound Score": "#1a1a1a",
|
||||
"Cuda": "#3A4E3A",
|
||||
"Curry": "#531242",
|
||||
"Cypher": "#34c0eb",
|
||||
"Cython": "#fedf5b",
|
||||
"D": "#ba595e",
|
||||
"DM": "#447265",
|
||||
@@ -115,6 +117,7 @@
|
||||
"DirectX 3D File": "#aace60",
|
||||
"Dockerfile": "#384d54",
|
||||
"Dogescript": "#cca760",
|
||||
"Dotenv": "#e5d559",
|
||||
"Dylan": "#6c616e",
|
||||
"E": "#ccce35",
|
||||
"ECL": "#8a1267",
|
||||
@@ -124,11 +127,13 @@
|
||||
"Earthly": "#2af0ff",
|
||||
"Easybuild": "#069406",
|
||||
"Ecere Projects": "#913960",
|
||||
"Ecmarkup": "#eb8131",
|
||||
"EditorConfig": "#fff1f2",
|
||||
"Eiffel": "#4d6977",
|
||||
"Elixir": "#6e4a7e",
|
||||
"Elm": "#60B5CC",
|
||||
"Elvish": "#55BB55",
|
||||
"Elvish Transcript": "#55BB55",
|
||||
"Emacs Lisp": "#c065db",
|
||||
"EmberScript": "#FFF4F3",
|
||||
"Erlang": "#B83998",
|
||||
@@ -147,7 +152,7 @@
|
||||
"Forth": "#341708",
|
||||
"Fortran": "#4d41b1",
|
||||
"Fortran Free Form": "#4d41b1",
|
||||
"FreeBasic": "#867db1",
|
||||
"FreeBasic": "#141AC9",
|
||||
"FreeMarker": "#0050b2",
|
||||
"Frege": "#00cafe",
|
||||
"Futhark": "#5f021f",
|
||||
@@ -180,6 +185,7 @@
|
||||
"Go": "#00ADD8",
|
||||
"Go Checksums": "#00ADD8",
|
||||
"Go Module": "#00ADD8",
|
||||
"Godot Resource": "#355570",
|
||||
"Golo": "#88562A",
|
||||
"Gosu": "#82937f",
|
||||
"Grace": "#615f8b",
|
||||
@@ -190,6 +196,7 @@
|
||||
"Groovy": "#4298b8",
|
||||
"Groovy Server Pages": "#4298b8",
|
||||
"HAProxy": "#106da9",
|
||||
"HCL": "#844FBA",
|
||||
"HLSL": "#aace60",
|
||||
"HOCON": "#9ff8ee",
|
||||
"HTML": "#e34c26",
|
||||
@@ -215,6 +222,7 @@
|
||||
"Idris": "#b30000",
|
||||
"Ignore List": "#000000",
|
||||
"ImageJ Macro": "#99AAFF",
|
||||
"Imba": "#16cec6",
|
||||
"Inno Setup": "#264b99",
|
||||
"Io": "#a9188d",
|
||||
"Ioke": "#078193",
|
||||
@@ -222,6 +230,7 @@
|
||||
"Isabelle ROOT": "#FEFE00",
|
||||
"J": "#9EEDFF",
|
||||
"JAR Manifest": "#b07219",
|
||||
"JCL": "#d90e09",
|
||||
"JFlex": "#DBCA00",
|
||||
"JSON": "#292929",
|
||||
"JSON with Comments": "#292929",
|
||||
@@ -244,9 +253,11 @@
|
||||
"Jsonnet": "#0064bd",
|
||||
"Julia": "#a270ba",
|
||||
"Jupyter Notebook": "#DA5B0B",
|
||||
"Just": "#384d54",
|
||||
"KRL": "#28430A",
|
||||
"Kaitai Struct": "#773b37",
|
||||
"KakouneScript": "#6f8042",
|
||||
"KerboScript": "#41adf0",
|
||||
"KiCad Layout": "#2f4aab",
|
||||
"KiCad Legacy Layout": "#2f4aab",
|
||||
"KiCad Schematic": "#2f4aab",
|
||||
@@ -286,6 +297,7 @@
|
||||
"Mathematica": "#dd1100",
|
||||
"Max": "#c4a79c",
|
||||
"Mercury": "#ff2b2b",
|
||||
"Mermaid": "#ff3670",
|
||||
"Meson": "#007800",
|
||||
"Metal": "#8f14e9",
|
||||
"MiniYAML": "#ff1111",
|
||||
@@ -318,6 +330,10 @@
|
||||
"Nu": "#c9df40",
|
||||
"NumPy": "#9C8AF9",
|
||||
"Nunjucks": "#3d8137",
|
||||
"OASv2-json": "#85ea2d",
|
||||
"OASv2-yaml": "#85ea2d",
|
||||
"OASv3-json": "#85ea2d",
|
||||
"OASv3-yaml": "#85ea2d",
|
||||
"OCaml": "#3be133",
|
||||
"ObjectScript": "#424893",
|
||||
"Objective-C": "#438eff",
|
||||
@@ -327,14 +343,18 @@
|
||||
"Omgrofl": "#cabbff",
|
||||
"Opal": "#f7ede0",
|
||||
"Open Policy Agent": "#7d9199",
|
||||
"OpenAPI Specification v2": "#85ea2d",
|
||||
"OpenAPI Specification v3": "#85ea2d",
|
||||
"OpenCL": "#ed2e2d",
|
||||
"OpenEdge ABL": "#5ce600",
|
||||
"OpenQASM": "#AA70FF",
|
||||
"OpenSCAD": "#e5cd45",
|
||||
"Option List": "#476732",
|
||||
"Org": "#77aa99",
|
||||
"Oxygene": "#cdd0e3",
|
||||
"Oz": "#fab738",
|
||||
"P4": "#7055b5",
|
||||
"PDDL": "#0d00ff",
|
||||
"PEG.js": "#234d6b",
|
||||
"PHP": "#4F5D95",
|
||||
"PLSQL": "#dad8d8",
|
||||
@@ -350,7 +370,9 @@
|
||||
"PicoLisp": "#6067af",
|
||||
"PigLatin": "#fcd7de",
|
||||
"Pike": "#005390",
|
||||
"PlantUML": "#fbbd16",
|
||||
"PogoScript": "#d80074",
|
||||
"Polar": "#ae81ff",
|
||||
"Portugol": "#f8bd00",
|
||||
"PostCSS": "#dc3a0c",
|
||||
"PostScript": "#da291c",
|
||||
@@ -366,6 +388,7 @@
|
||||
"Puppet": "#302B6D",
|
||||
"PureBasic": "#5a6986",
|
||||
"PureScript": "#1D222D",
|
||||
"Pyret": "#ee1e10",
|
||||
"Python": "#3572A5",
|
||||
"Python console": "#3572A5",
|
||||
"Python traceback": "#3572A5",
|
||||
@@ -414,6 +437,7 @@
|
||||
"Sass": "#a53b70",
|
||||
"Scala": "#c22d40",
|
||||
"Scaml": "#bd181a",
|
||||
"Scenic": "#fdc700",
|
||||
"Scheme": "#1e4aec",
|
||||
"Scilab": "#ca0f21",
|
||||
"Self": "#0579aa",
|
||||
@@ -421,6 +445,7 @@
|
||||
"Shell": "#89e051",
|
||||
"ShellCheck Config": "#cecfcb",
|
||||
"Shen": "#120F14",
|
||||
"Simple File Verification": "#C9BFED",
|
||||
"Singularity": "#64E6AD",
|
||||
"Slash": "#007eff",
|
||||
"Slice": "#003fa2",
|
||||
@@ -428,6 +453,8 @@
|
||||
"SmPL": "#c94949",
|
||||
"Smalltalk": "#596706",
|
||||
"Smarty": "#f0c040",
|
||||
"Smithy": "#c44536",
|
||||
"Snakemake": "#419179",
|
||||
"Solidity": "#AA6746",
|
||||
"SourcePawn": "#f69e1d",
|
||||
"Squirrel": "#800000",
|
||||
@@ -441,6 +468,7 @@
|
||||
"SugarSS": "#2fcc9f",
|
||||
"SuperCollider": "#46390b",
|
||||
"Svelte": "#ff3e00",
|
||||
"Sway": "#dea584",
|
||||
"Swift": "#F05138",
|
||||
"SystemVerilog": "#DAE1C2",
|
||||
"TI Program": "#A0AA87",
|
||||
@@ -478,6 +506,7 @@
|
||||
"Vim Script": "#199f4b",
|
||||
"Vim Snippet": "#199f4b",
|
||||
"Visual Basic .NET": "#945db7",
|
||||
"Visual Basic 6.0": "#2c6353",
|
||||
"Volt": "#1F1F1F",
|
||||
"Vue": "#41b883",
|
||||
"Vyper": "#2980b9",
|
||||
|
||||
@@ -45,7 +45,9 @@ const retryer = async (fetcher, variables, retries = 0) => {
|
||||
// prettier-ignore
|
||||
// also checking for bad credentials if any tokens gets invalidated
|
||||
const isBadCredential = err.response.data && err.response.data.message === "Bad credentials";
|
||||
const isAccountSuspended = err.response.data && err.response.data.message === "Sorry. Your account was suspended.";
|
||||
const isAccountSuspended =
|
||||
err.response.data &&
|
||||
err.response.data.message === "Sorry. Your account was suspended.";
|
||||
|
||||
if (isBadCredential || isAccountSuspended) {
|
||||
logger.log(`PAT_${retries + 1} Failed`);
|
||||
|
||||
+17
-3
@@ -194,15 +194,15 @@ const flexLayout = ({ items, gap, direction, sizes = [] }) => {
|
||||
/**
|
||||
* Returns theme based colors with proper overrides and defaults.
|
||||
*
|
||||
* @param {Object[]} args Function arguments.
|
||||
* @param {Object} args Function arguments.
|
||||
* @param {string} args.title_color Card title color.
|
||||
* @param {string} args.text_color Card text color.
|
||||
* @param {string} args.icon_color Card icon color.
|
||||
* @param {string} args.bg_color Card background color.
|
||||
* @param {string} args.border_color Card border color.
|
||||
* @param {string} args.ring_color Card ring color.
|
||||
* @param {string} args.theme Card theme.
|
||||
* @param {string} args.fallbackTheme Fallback theme.
|
||||
*
|
||||
*/
|
||||
const getCardColors = ({
|
||||
title_color,
|
||||
@@ -392,7 +392,7 @@ const lowercaseTrim = (name) => name.toLowerCase().trim();
|
||||
/**
|
||||
* Split array of languages in two columns.
|
||||
*
|
||||
* @template T Langauge object.
|
||||
* @template T Language object.
|
||||
* @param {Array<T>} arr Array of languages.
|
||||
* @param {number} perChunk Number of languages per column.
|
||||
* @returns {Array<T>} Array of languages split in two columns.
|
||||
@@ -424,6 +424,19 @@ 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 {
|
||||
ERROR_CARD_LENGTH,
|
||||
renderError,
|
||||
@@ -447,4 +460,5 @@ export {
|
||||
lowercaseTrim,
|
||||
chunkArray,
|
||||
parseEmojis,
|
||||
dateDiff,
|
||||
};
|
||||
|
||||
+103
-105
@@ -1,5 +1,6 @@
|
||||
// @ts-check
|
||||
import axios from "axios";
|
||||
import * as dotenv from "dotenv";
|
||||
import githubUsernameRegex from "github-username-regex";
|
||||
import { calculateRank } from "../calculateRank.js";
|
||||
import { retryer } from "../common/retryer.js";
|
||||
@@ -11,46 +12,74 @@ import {
|
||||
wrapTextMultiline,
|
||||
} 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.
|
||||
*
|
||||
* @param {import('axios').AxiosRequestHeaders} variables Fetcher variables.
|
||||
* @param {string} token GitHub token.
|
||||
* @returns {Promise<import('../common/types').StatsFetcherResponse>} Stats fetcher response.
|
||||
* @returns {Promise<import('../common/types').Fetcher>} Stats fetcher response.
|
||||
*/
|
||||
const fetcher = (variables, token) => {
|
||||
const query = !variables.after ? GRAPHQL_STATS_QUERY : GRAPHQL_REPOS_QUERY;
|
||||
return request(
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
query,
|
||||
variables,
|
||||
},
|
||||
{
|
||||
@@ -60,39 +89,42 @@ const fetcher = (variables, token) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch first 100 repositories for a given username.
|
||||
* Fetch stats information for a given username.
|
||||
*
|
||||
* @param {import('axios').AxiosRequestHeaders} variables Fetcher variables.
|
||||
* @param {string} token GitHub token.
|
||||
* @returns {Promise<import('../common/types').StatsFetcherResponse>} Repositories fetcher response.
|
||||
* @param {string} username Github username.
|
||||
* @returns {Promise<import('../common/types').StatsFetcher>} GraphQL Stats object.
|
||||
*
|
||||
* @description This function supports multi-page fetching if the 'FETCH_MULTI_PAGE_STARS' environment variable is set to true.
|
||||
*/
|
||||
const repositoriesFetcher = (variables, token) => {
|
||||
return request(
|
||||
{
|
||||
query: `
|
||||
query userInfo($login: String!, $after: String) {
|
||||
user(login: $login) {
|
||||
repositories(first: 100, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}, after: $after) {
|
||||
nodes {
|
||||
name
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables,
|
||||
},
|
||||
{
|
||||
Authorization: `bearer ${token}`,
|
||||
},
|
||||
);
|
||||
const statsFetcher = async (username) => {
|
||||
let stats;
|
||||
let hasNextPage = true;
|
||||
let endCursor = null;
|
||||
while (hasNextPage) {
|
||||
const variables = { login: username, first: 100, after: endCursor };
|
||||
let res = await retryer(fetcher, variables);
|
||||
if (res.data.errors) return res;
|
||||
|
||||
// Store stats data.
|
||||
const repoNodes = res.data.data.user.repositories.nodes;
|
||||
if (!stats) {
|
||||
stats = res;
|
||||
} else {
|
||||
stats.data.data.user.repositories.nodes.push(...repoNodes);
|
||||
}
|
||||
|
||||
// Disable multi page fetching on public Vercel instance due to rate limits.
|
||||
const repoNodesWithStars = repoNodes.filter(
|
||||
(node) => node.stargazers.totalCount !== 0,
|
||||
);
|
||||
hasNextPage =
|
||||
process.env.FETCH_MULTI_PAGE_STARS === "true" &&
|
||||
repoNodes.length === repoNodesWithStars.length &&
|
||||
res.data.data.user.repositories.pageInfo.hasNextPage;
|
||||
endCursor = res.data.data.user.repositories.pageInfo.endCursor;
|
||||
}
|
||||
|
||||
return stats;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -137,46 +169,6 @@ const totalCommitsFetcher = async (username) => {
|
||||
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.
|
||||
*
|
||||
@@ -203,7 +195,7 @@ const fetchStats = async (
|
||||
rank: { level: "C", score: 0 },
|
||||
};
|
||||
|
||||
let res = await retryer(fetcher, { login: username });
|
||||
let res = await statsFetcher(username);
|
||||
|
||||
// Catch GraphQL errors.
|
||||
if (res.data.errors) {
|
||||
@@ -221,7 +213,7 @@ const fetchStats = async (
|
||||
);
|
||||
}
|
||||
throw new CustomError(
|
||||
"Something went while trying to retrieve the stats data using the GraphQL API.",
|
||||
"Something went wrong while trying to retrieve the stats data using the GraphQL API.",
|
||||
CustomError.GRAPHQL_ERROR,
|
||||
);
|
||||
}
|
||||
@@ -259,7 +251,13 @@ const fetchStats = async (
|
||||
stats.contributedTo = user.repositoriesContributedTo.totalCount;
|
||||
|
||||
// Retrieve stars while filtering out repositories to be hidden
|
||||
stats.totalStars = await totalStarsFetcher(username, repoToHide);
|
||||
stats.totalStars = user.repositories.nodes
|
||||
.filter((data) => {
|
||||
return !repoToHide[data.name];
|
||||
})
|
||||
.reduce((prev, curr) => {
|
||||
return prev + curr.stargazers.totalCount;
|
||||
}, 0);
|
||||
|
||||
stats.rank = calculateRank({
|
||||
totalCommits: stats.totalCommits,
|
||||
|
||||
@@ -54,7 +54,12 @@ const fetcher = (variables, token) => {
|
||||
* @param {string[]} exclude_repo List of repositories to exclude.
|
||||
* @returns {Promise<import("./types").TopLangData>} Top languages data.
|
||||
*/
|
||||
const fetchTopLanguages = async (username, exclude_repo = []) => {
|
||||
const fetchTopLanguages = async (
|
||||
username,
|
||||
exclude_repo = [],
|
||||
size_weight = 1,
|
||||
count_weight = 0,
|
||||
) => {
|
||||
if (!username) throw new MissingParamError(["username"]);
|
||||
|
||||
const res = await retryer(fetcher, { login: username });
|
||||
@@ -101,6 +106,8 @@ const fetchTopLanguages = async (username, exclude_repo = []) => {
|
||||
.sort((a, b) => b.size - a.size)
|
||||
.filter((name) => !repoToHide[name.name]);
|
||||
|
||||
let repoCount = 0;
|
||||
|
||||
repoNodes = repoNodes
|
||||
.filter((node) => node.languages.edges.length > 0)
|
||||
// flatten the list of language nodes
|
||||
@@ -111,9 +118,14 @@ const fetchTopLanguages = async (username, exclude_repo = []) => {
|
||||
|
||||
// if we already have the language in the accumulator
|
||||
// & the current language name is same as previous name
|
||||
// add the size to the language size.
|
||||
// add the size to the language size and increase repoCount.
|
||||
if (acc[prev.node.name] && prev.node.name === acc[prev.node.name].name) {
|
||||
langSize = prev.size + acc[prev.node.name].size;
|
||||
repoCount += 1;
|
||||
} else {
|
||||
// reset repoCount to 1
|
||||
// language must exist in at least one repo to be detected
|
||||
repoCount = 1;
|
||||
}
|
||||
return {
|
||||
...acc,
|
||||
@@ -121,10 +133,18 @@ const fetchTopLanguages = async (username, exclude_repo = []) => {
|
||||
name: prev.node.name,
|
||||
color: prev.node.color,
|
||||
size: langSize,
|
||||
count: repoCount,
|
||||
},
|
||||
};
|
||||
}, {});
|
||||
|
||||
Object.keys(repoNodes).forEach((name) => {
|
||||
// comparison index calculation
|
||||
repoNodes[name].size =
|
||||
Math.pow(repoNodes[name].size, size_weight) *
|
||||
Math.pow(repoNodes[name].count, count_weight);
|
||||
});
|
||||
|
||||
const topLangs = Object.keys(repoNodes)
|
||||
.sort((a, b) => repoNodes[b].size - repoNodes[a].size)
|
||||
.reduce((result, key) => {
|
||||
|
||||
@@ -14,7 +14,9 @@ const fetchWakatimeStats = async ({ username, api_domain, range }) => {
|
||||
const { data } = await axios.get(
|
||||
`https://${
|
||||
api_domain ? api_domain.replace(/\/$/gi, "") : "wakatime.com"
|
||||
}/api/v1/users/${username}/stats/${range || ""}?is_including_today=true`,
|
||||
}/api/v1/users/${username}/stats/${
|
||||
range || "all_time"
|
||||
}?is_including_today=true`,
|
||||
);
|
||||
|
||||
return data.data;
|
||||
|
||||
@@ -69,7 +69,29 @@ exports[`Test Render Wakatime Card should render correctly with compact layout 1
|
||||
}
|
||||
|
||||
|
||||
@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: #434d58 }
|
||||
#rect-mask rect{
|
||||
animation: slideInAnimation 1s ease-in-out forwards;
|
||||
}
|
||||
.lang-progress{
|
||||
animation: growWidthAnimation 0.6s ease-in-out forwards;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -227,7 +249,29 @@ exports[`Test Render Wakatime Card should render correctly with compact layout w
|
||||
}
|
||||
|
||||
|
||||
@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: #434d58 }
|
||||
#rect-mask rect{
|
||||
animation: slideInAnimation 1s ease-in-out forwards;
|
||||
}
|
||||
.lang-progress{
|
||||
animation: growWidthAnimation 0.6s ease-in-out forwards;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
+12
-25
@@ -25,7 +25,7 @@ stats.rank = calculateRank({
|
||||
issues: stats.totalIssues,
|
||||
});
|
||||
|
||||
const data = {
|
||||
const data_stats = {
|
||||
data: {
|
||||
user: {
|
||||
name: stats.name,
|
||||
@@ -40,19 +40,10 @@ const data = {
|
||||
followers: { totalCount: 0 },
|
||||
repositories: {
|
||||
totalCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const repositoriesData = {
|
||||
data: {
|
||||
user: {
|
||||
repositories: {
|
||||
nodes: [{ stargazers: { totalCount: 100 } }],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
cursor: "cursor",
|
||||
endCursor: "cursor",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -83,11 +74,7 @@ const faker = (query, data) => {
|
||||
setHeader: jest.fn(),
|
||||
send: jest.fn(),
|
||||
};
|
||||
mock
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.replyOnce(200, data)
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.replyOnce(200, repositoriesData);
|
||||
mock.onPost("https://api.github.com/graphql").replyOnce(200, data);
|
||||
|
||||
return { req, res };
|
||||
};
|
||||
@@ -98,7 +85,7 @@ afterEach(() => {
|
||||
|
||||
describe("Test /api/", () => {
|
||||
it("should test the request", async () => {
|
||||
const { req, res } = faker({}, data);
|
||||
const { req, res } = faker({}, data_stats);
|
||||
|
||||
await api(req, res);
|
||||
|
||||
@@ -133,7 +120,7 @@ describe("Test /api/", () => {
|
||||
text_color: "fff",
|
||||
bg_color: "fff",
|
||||
},
|
||||
data,
|
||||
data_stats,
|
||||
);
|
||||
|
||||
await api(req, res);
|
||||
@@ -154,7 +141,7 @@ describe("Test /api/", () => {
|
||||
});
|
||||
|
||||
it("should have proper cache", async () => {
|
||||
const { req, res } = faker({}, data);
|
||||
const { req, res } = faker({}, data_stats);
|
||||
|
||||
await api(req, res);
|
||||
|
||||
@@ -170,7 +157,7 @@ describe("Test /api/", () => {
|
||||
});
|
||||
|
||||
it("should set proper cache", async () => {
|
||||
const { req, res } = faker({ cache_seconds: 15000 }, data);
|
||||
const { req, res } = faker({ cache_seconds: 15000 }, data_stats);
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader.mock.calls).toEqual([
|
||||
@@ -196,7 +183,7 @@ describe("Test /api/", () => {
|
||||
|
||||
it("should set proper cache with clamped values", async () => {
|
||||
{
|
||||
let { req, res } = faker({ cache_seconds: 200000 }, data);
|
||||
let { req, res } = faker({ cache_seconds: 200000 }, data_stats);
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader.mock.calls).toEqual([
|
||||
@@ -212,7 +199,7 @@ describe("Test /api/", () => {
|
||||
|
||||
// note i'm using block scoped vars
|
||||
{
|
||||
let { req, res } = faker({ cache_seconds: 0 }, data);
|
||||
let { req, res } = faker({ cache_seconds: 0 }, data_stats);
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader.mock.calls).toEqual([
|
||||
@@ -227,7 +214,7 @@ describe("Test /api/", () => {
|
||||
}
|
||||
|
||||
{
|
||||
let { req, res } = faker({ cache_seconds: -10000 }, data);
|
||||
let { req, res } = faker({ cache_seconds: -10000 }, data_stats);
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader.mock.calls).toEqual([
|
||||
@@ -248,7 +235,7 @@ describe("Test /api/", () => {
|
||||
username: "anuraghazra",
|
||||
count_private: true,
|
||||
},
|
||||
data,
|
||||
data_stats,
|
||||
);
|
||||
|
||||
await api(req, res);
|
||||
@@ -288,7 +275,7 @@ describe("Test /api/", () => {
|
||||
text_color: "fff",
|
||||
bg_color: "fff",
|
||||
},
|
||||
data,
|
||||
data_stats,
|
||||
);
|
||||
|
||||
await api(req, res);
|
||||
|
||||
+19
-24
@@ -11,27 +11,22 @@ import { renderStatsCard } from "../../src/cards/stats-card.js";
|
||||
import { renderTopLanguages } from "../../src/cards/top-languages-card.js";
|
||||
import { renderWakatimeCard } from "../../src/cards/wakatime-card.js";
|
||||
|
||||
const REPO = "dummy-cra";
|
||||
const USER = "grsdummy";
|
||||
const REPO = "curly-fiesta";
|
||||
const USER = "catelinemnemosyne";
|
||||
const STATS_DATA = {
|
||||
name: "grsdummy",
|
||||
name: "Cateline Mnemosyne",
|
||||
totalPRs: 2,
|
||||
totalCommits: 2,
|
||||
totalCommits: 8,
|
||||
totalIssues: 1,
|
||||
totalStars: 1,
|
||||
contributedTo: 2,
|
||||
contributedTo: 1,
|
||||
rank: {
|
||||
level: "A+",
|
||||
score: 50.900829325065935,
|
||||
score: 50.88831151384285,
|
||||
},
|
||||
};
|
||||
|
||||
const LANGS_DATA = {
|
||||
TypeScript: {
|
||||
color: "#3178c6",
|
||||
name: "TypeScript",
|
||||
size: 2049,
|
||||
},
|
||||
HTML: {
|
||||
color: "#e34c26",
|
||||
name: "HTML",
|
||||
@@ -42,19 +37,19 @@ const LANGS_DATA = {
|
||||
name: "CSS",
|
||||
size: 930,
|
||||
},
|
||||
Python: {
|
||||
color: "#3572A5",
|
||||
name: "Python",
|
||||
size: 671,
|
||||
JavaScript: {
|
||||
color: "#f1e05a",
|
||||
name: "JavaScript",
|
||||
size: 1912,
|
||||
},
|
||||
};
|
||||
|
||||
const WAKATIME_DATA = {
|
||||
human_readable_range: "last week",
|
||||
is_already_updating: false,
|
||||
is_coding_activity_visible: false,
|
||||
is_coding_activity_visible: true,
|
||||
is_including_today: false,
|
||||
is_other_usage_visible: false,
|
||||
is_other_usage_visible: true,
|
||||
is_stuck: false,
|
||||
is_up_to_date: false,
|
||||
is_up_to_date_pending_future: false,
|
||||
@@ -62,24 +57,24 @@ const WAKATIME_DATA = {
|
||||
range: "last_7_days",
|
||||
status: "pending_update",
|
||||
timeout: 15,
|
||||
username: "grsdummy",
|
||||
username: USER,
|
||||
writes_only: false,
|
||||
};
|
||||
|
||||
const REPOSITORY_DATA = {
|
||||
name: "dummy-cra",
|
||||
nameWithOwner: "grsdummy/dummy-cra",
|
||||
name: REPO,
|
||||
nameWithOwner: `${USER}/cra-test`,
|
||||
isPrivate: false,
|
||||
isArchived: false,
|
||||
isTemplate: false,
|
||||
stargazers: {
|
||||
totalCount: 1,
|
||||
},
|
||||
description: "Dummy create react app.",
|
||||
description: "Simple cra test repo.",
|
||||
primaryLanguage: {
|
||||
color: "#3178c6",
|
||||
id: "MDg6TGFuZ3VhZ2UyODc=",
|
||||
name: "TypeScript",
|
||||
color: "#f1e05a",
|
||||
id: "MDg6TGFuZ3VhZ2UxNDA=",
|
||||
name: "JavaScript",
|
||||
},
|
||||
forkCount: 0,
|
||||
starCount: 1,
|
||||
|
||||
+99
-40
@@ -4,7 +4,8 @@ import MockAdapter from "axios-mock-adapter";
|
||||
import { calculateRank } from "../src/calculateRank.js";
|
||||
import { fetchStats } from "../src/fetchers/stats-fetcher.js";
|
||||
|
||||
const data = {
|
||||
// Test parameters.
|
||||
const data_stats = {
|
||||
data: {
|
||||
user: {
|
||||
name: "Anurag Hazra",
|
||||
@@ -19,15 +20,6 @@ const data = {
|
||||
followers: { totalCount: 100 },
|
||||
repositories: {
|
||||
totalCount: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const firstRepositoriesData = {
|
||||
data: {
|
||||
user: {
|
||||
repositories: {
|
||||
nodes: [
|
||||
{ name: "test-repo-1", stargazers: { totalCount: 100 } },
|
||||
{ name: "test-repo-2", stargazers: { totalCount: 100 } },
|
||||
@@ -35,14 +27,14 @@ const firstRepositoriesData = {
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
cursor: "cursor",
|
||||
endCursor: "cursor",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const secondRepositoriesData = {
|
||||
const data_repo = {
|
||||
data: {
|
||||
user: {
|
||||
repositories: {
|
||||
@@ -52,14 +44,14 @@ const secondRepositoriesData = {
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
cursor: "cursor",
|
||||
endCursor: "cursor",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const repositoriesWithZeroStarsData = {
|
||||
const data_repo_zero_stars = {
|
||||
data: {
|
||||
user: {
|
||||
repositories: {
|
||||
@@ -72,7 +64,7 @@ const repositoriesWithZeroStarsData = {
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
cursor: "cursor",
|
||||
endCursor: "cursor",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -93,13 +85,13 @@ const error = {
|
||||
const mock = new MockAdapter(axios);
|
||||
|
||||
beforeEach(() => {
|
||||
mock
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.replyOnce(200, data)
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.replyOnce(200, firstRepositoriesData);
|
||||
// .onPost("https://api.github.com/graphql") // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
// .replyOnce(200, secondRepositoriesData); // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
process.env.FETCH_MULTI_PAGE_STARS = "false"; // Set to `false` to fetch only one page of stars.
|
||||
mock.onPost("https://api.github.com/graphql").reply((cfg) => {
|
||||
return [
|
||||
200,
|
||||
cfg.data.includes("contributionsCollection") ? data_stats : data_repo,
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -114,8 +106,7 @@ describe("Test fetchStats", () => {
|
||||
totalRepos: 5,
|
||||
followers: 100,
|
||||
contributions: 61,
|
||||
// stargazers: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
stargazers: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
stargazers: 300,
|
||||
prs: 300,
|
||||
issues: 200,
|
||||
});
|
||||
@@ -126,8 +117,7 @@ describe("Test fetchStats", () => {
|
||||
totalCommits: 100,
|
||||
totalIssues: 200,
|
||||
totalPRs: 300,
|
||||
// totalStars: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
totalStars: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
totalStars: 300,
|
||||
rank,
|
||||
});
|
||||
});
|
||||
@@ -136,9 +126,9 @@ describe("Test fetchStats", () => {
|
||||
mock.reset();
|
||||
mock
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.replyOnce(200, data)
|
||||
.replyOnce(200, data_stats)
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.replyOnce(200, repositoriesWithZeroStarsData);
|
||||
.replyOnce(200, data_repo_zero_stars);
|
||||
|
||||
let stats = await fetchStats("anuraghazra");
|
||||
const rank = calculateRank({
|
||||
@@ -178,8 +168,7 @@ describe("Test fetchStats", () => {
|
||||
totalRepos: 5,
|
||||
followers: 100,
|
||||
contributions: 61,
|
||||
// stargazers: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
stargazers: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
stargazers: 300,
|
||||
prs: 300,
|
||||
issues: 200,
|
||||
});
|
||||
@@ -190,8 +179,7 @@ describe("Test fetchStats", () => {
|
||||
totalCommits: 150,
|
||||
totalIssues: 200,
|
||||
totalPRs: 300,
|
||||
// totalStars: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
totalStars: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
totalStars: 300,
|
||||
rank,
|
||||
});
|
||||
});
|
||||
@@ -207,8 +195,7 @@ describe("Test fetchStats", () => {
|
||||
totalRepos: 5,
|
||||
followers: 100,
|
||||
contributions: 61,
|
||||
// stargazers: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
stargazers: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
stargazers: 300,
|
||||
prs: 300,
|
||||
issues: 200,
|
||||
});
|
||||
@@ -219,8 +206,7 @@ describe("Test fetchStats", () => {
|
||||
totalCommits: 1050,
|
||||
totalIssues: 200,
|
||||
totalPRs: 300,
|
||||
// totalStars: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
totalStars: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
totalStars: 300,
|
||||
rank,
|
||||
});
|
||||
});
|
||||
@@ -236,8 +222,7 @@ describe("Test fetchStats", () => {
|
||||
totalRepos: 5,
|
||||
followers: 100,
|
||||
contributions: 61,
|
||||
// stargazers: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
stargazers: 200, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
stargazers: 200,
|
||||
prs: 300,
|
||||
issues: 200,
|
||||
});
|
||||
@@ -248,8 +233,82 @@ describe("Test fetchStats", () => {
|
||||
totalCommits: 1050,
|
||||
totalIssues: 200,
|
||||
totalPRs: 300,
|
||||
// totalStars: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
totalStars: 200, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
totalStars: 200,
|
||||
rank,
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,20 +60,22 @@ const error = {
|
||||
};
|
||||
|
||||
describe("FetchTopLanguages", () => {
|
||||
it("should fetch correct language data", async () => {
|
||||
it("should fetch correct language data while using the new calculation", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
|
||||
|
||||
let repo = await fetchTopLanguages("anuraghazra");
|
||||
let repo = await fetchTopLanguages("anuraghazra", [], 0.5, 0.5);
|
||||
expect(repo).toStrictEqual({
|
||||
HTML: {
|
||||
color: "#0f0",
|
||||
count: 2,
|
||||
name: "HTML",
|
||||
size: 200,
|
||||
size: 20.000000000000004,
|
||||
},
|
||||
javascript: {
|
||||
color: "#0ff",
|
||||
count: 2,
|
||||
name: "javascript",
|
||||
size: 200,
|
||||
size: 20.000000000000004,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -85,17 +87,59 @@ describe("FetchTopLanguages", () => {
|
||||
expect(repo).toStrictEqual({
|
||||
HTML: {
|
||||
color: "#0f0",
|
||||
count: 1,
|
||||
name: "HTML",
|
||||
size: 100,
|
||||
},
|
||||
javascript: {
|
||||
color: "#0ff",
|
||||
count: 2,
|
||||
name: "javascript",
|
||||
size: 200,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch correct language data while using the old calculation", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
|
||||
|
||||
let repo = await fetchTopLanguages("anuraghazra", [], 1, 0);
|
||||
expect(repo).toStrictEqual({
|
||||
HTML: {
|
||||
color: "#0f0",
|
||||
count: 2,
|
||||
name: "HTML",
|
||||
size: 200,
|
||||
},
|
||||
javascript: {
|
||||
color: "#0ff",
|
||||
count: 2,
|
||||
name: "javascript",
|
||||
size: 200,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should rank languages by the number of repositories they appear in", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
|
||||
|
||||
let repo = await fetchTopLanguages("anuraghazra", [], 0, 1);
|
||||
expect(repo).toStrictEqual({
|
||||
HTML: {
|
||||
color: "#0f0",
|
||||
count: 2,
|
||||
name: "HTML",
|
||||
size: 2,
|
||||
},
|
||||
javascript: {
|
||||
color: "#0ff",
|
||||
count: 2,
|
||||
name: "javascript",
|
||||
size: 2,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw error", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, error);
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ describe("Wakatime fetcher", () => {
|
||||
const username = "anuraghazra";
|
||||
mock
|
||||
.onGet(
|
||||
`https://wakatime.com/api/v1/users/${username}/stats/?is_including_today=true`,
|
||||
`https://wakatime.com/api/v1/users/${username}/stats/all_time?is_including_today=true`,
|
||||
)
|
||||
.reply(200, wakaTimeData);
|
||||
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* @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,16 +78,17 @@ describe("Test renderStatsCard", () => {
|
||||
|
||||
it("should render with custom width set", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats);
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "495");
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "450");
|
||||
|
||||
document.body.innerHTML = renderStatsCard(stats, { card_width: 400 });
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "400");
|
||||
document.body.innerHTML = renderStatsCard(stats, { card_width: 500 });
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "500");
|
||||
});
|
||||
|
||||
it("should render with custom width set and limit minimum width", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, { card_width: 1 });
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "340");
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "420");
|
||||
|
||||
// Test default minimum card width without rank circle.
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
card_width: 1,
|
||||
hide_rank: true,
|
||||
@@ -97,6 +98,7 @@ describe("Test renderStatsCard", () => {
|
||||
"305.81250000000006",
|
||||
);
|
||||
|
||||
// Test minimum card width with rank and icons.
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
card_width: 1,
|
||||
hide_rank: true,
|
||||
@@ -104,22 +106,24 @@ describe("Test renderStatsCard", () => {
|
||||
});
|
||||
expect(document.querySelector("svg")).toHaveAttribute(
|
||||
"width",
|
||||
"305.81250000000006",
|
||||
"322.81250000000006",
|
||||
);
|
||||
|
||||
// Test minimum card width with icons but without rank.
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
card_width: 1,
|
||||
hide_rank: false,
|
||||
show_icons: true,
|
||||
});
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "356");
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "437");
|
||||
|
||||
// Test minimum card width without icons or rank.
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
card_width: 1,
|
||||
hide_rank: false,
|
||||
show_icons: false,
|
||||
});
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "340");
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "420");
|
||||
});
|
||||
|
||||
it("should render default colors properly", () => {
|
||||
@@ -312,7 +316,7 @@ describe("Test renderStatsCard", () => {
|
||||
|
||||
expect(
|
||||
document.body.getElementsByTagName("svg")[0].getAttribute("width"),
|
||||
).toBe("270");
|
||||
).toBe("287");
|
||||
});
|
||||
|
||||
it("should render translations", () => {
|
||||
@@ -353,4 +357,30 @@ describe("Test renderStatsCard", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, {});
|
||||
expect(document.querySelector("rect")).toHaveAttribute("rx", "4.5");
|
||||
});
|
||||
|
||||
it("should shorten values", () => {
|
||||
stats["totalCommits"] = 1999;
|
||||
|
||||
document.body.innerHTML = renderStatsCard(stats);
|
||||
expect(getByTestId(document.body, "commits").textContent).toBe("2k");
|
||||
document.body.innerHTML = renderStatsCard(stats, { number_format: "long" });
|
||||
expect(getByTestId(document.body, "commits").textContent).toBe("1999");
|
||||
});
|
||||
|
||||
it("should render default rank icon with level A+", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
rank_icon: "default",
|
||||
});
|
||||
expect(queryByTestId(document.body, "level-rank-icon")).toBeDefined();
|
||||
expect(
|
||||
queryByTestId(document.body, "level-rank-icon").textContent.trim(),
|
||||
).toBe("A+");
|
||||
});
|
||||
|
||||
it("should render github rank icon", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
rank_icon: "github",
|
||||
});
|
||||
expect(queryByTestId(document.body, "github-rank-icon")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -216,7 +216,7 @@ describe("Test renderTopLanguages", () => {
|
||||
);
|
||||
expect(queryAllByTestId(document.body, "lang-progress")[0]).toHaveAttribute(
|
||||
"width",
|
||||
"120",
|
||||
"100",
|
||||
);
|
||||
|
||||
expect(queryAllByTestId(document.body, "lang-name")[1]).toHaveTextContent(
|
||||
@@ -224,7 +224,7 @@ describe("Test renderTopLanguages", () => {
|
||||
);
|
||||
expect(queryAllByTestId(document.body, "lang-progress")[1]).toHaveAttribute(
|
||||
"width",
|
||||
"120",
|
||||
"100",
|
||||
);
|
||||
|
||||
expect(queryAllByTestId(document.body, "lang-name")[2]).toHaveTextContent(
|
||||
@@ -232,7 +232,7 @@ describe("Test renderTopLanguages", () => {
|
||||
);
|
||||
expect(queryAllByTestId(document.body, "lang-progress")[2]).toHaveAttribute(
|
||||
"width",
|
||||
"60",
|
||||
"50",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ describe("Test Render Wakatime Card", () => {
|
||||
expect(document.querySelector("rect")).toHaveAttribute("rx", "4.5");
|
||||
});
|
||||
|
||||
it('should show "no coding activitiy this week" message when there hasn not been activity', () => {
|
||||
it('should show "no coding activity this week" message when there has not been activity', () => {
|
||||
document.body.innerHTML = renderWakatimeCard(
|
||||
{
|
||||
...wakaTimeData.data,
|
||||
@@ -67,4 +67,19 @@ describe("Test Render Wakatime Card", () => {
|
||||
"No coding activity this week",
|
||||
);
|
||||
});
|
||||
|
||||
it('should show "no coding activity this week" message when using compact layout and there has not been activity', () => {
|
||||
document.body.innerHTML = renderWakatimeCard(
|
||||
{
|
||||
...wakaTimeData.data,
|
||||
languages: undefined,
|
||||
},
|
||||
{
|
||||
layout: "compact",
|
||||
},
|
||||
);
|
||||
expect(document.querySelector(".stat").textContent).toBe(
|
||||
"No coding activity this week",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* @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"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
+8
-6
@@ -32,10 +32,11 @@ Use `?theme=THEME_NAME` parameter like so :-
|
||||
| `jolly` ![jolly][jolly] | `maroongold` ![maroongold][maroongold] | `yeblu` ![yeblu][yeblu] |
|
||||
| `blueberry` ![blueberry][blueberry] | `slateorange` ![slateorange][slateorange] | `kacho_ga` ![kacho_ga][kacho_ga] |
|
||||
| `outrun` ![outrun][outrun] | `ocean_dark` ![ocean_dark][ocean_dark] | `city_lights` ![city_lights][city_lights] |
|
||||
| `github_dark` ![github_dark][github_dark] | `discord_old_blurple` ![discord_old_blurple][discord_old_blurple] | `aura_dark` ![aura_dark][aura_dark] |
|
||||
| `github_dark` ![github_dark][github_dark] | `github_dark_dimmed` ![github_dark_dimmed][github_dark_dimmed] | `discord_old_blurple` ![discord_old_blurple][discord_old_blurple] |
|
||||
| `panda` ![panda][panda] | `noctis_minimus` ![noctis_minimus][noctis_minimus] | `cobalt2` ![cobalt2][cobalt2] |
|
||||
| `swift` ![swift][swift] | `aura` ![aura][aura] | `apprentice` ![apprentice][apprentice] |
|
||||
| `moltack` ![moltack][moltack] | `codeSTACKr` ![codeSTACKr][codeSTACKr] | `rose_pine` ![rose_pine][rose_pine] |
|
||||
| `aura_dark` ![aura_dark][aura_dark] | | |
|
||||
| [Add your theme][add-theme] | | |
|
||||
|
||||
## Repo Card
|
||||
@@ -60,10 +61,11 @@ Use `?theme=THEME_NAME` parameter like so :-
|
||||
| `jolly` ![jolly][jolly_repo] | `maroongold` ![maroongold][maroongold_repo] | `yeblu` ![yeblu][yeblu_repo] |
|
||||
| `blueberry` ![blueberry][blueberry_repo] | `slateorange` ![slateorange][slateorange_repo] | `kacho_ga` ![kacho_ga][kacho_ga_repo] |
|
||||
| `outrun` ![outrun][outrun_repo] | `ocean_dark` ![ocean_dark][ocean_dark_repo] | `city_lights` ![city_lights][city_lights_repo] |
|
||||
| `github_dark` ![github_dark][github_dark_repo] | `discord_old_blurple` ![discord_old_blurple][discord_old_blurple_repo] | `aura_dark` ![aura_dark][aura_dark_repo] |
|
||||
| `github_dark` ![github_dark][github_dark_repo] | `github_dark_dimmed` ![github_dark_dimmed][github_dark_dimmed_repo] | `discord_old_blurple` ![discord_old_blurple][discord_old_blurple_repo] |
|
||||
| `panda` ![panda][panda_repo] | `noctis_minimus` ![noctis_minimus][noctis_minimus_repo] | `cobalt2` ![cobalt2][cobalt2_repo] |
|
||||
| `swift` ![swift][swift_repo] | `aura` ![aura][aura_repo] | `apprentice` ![apprentice][apprentice_repo] |
|
||||
| `moltack` ![moltack][moltack_repo] | `codeSTACKr` ![codeSTACKr][codeSTACKr_repo] | `rose_pine` ![rose_pine][rose_pine_repo] |
|
||||
| `aura_dark` ![aura_dark][aura_dark_repo] | | |
|
||||
| [Add your theme][add-theme] | | |
|
||||
|
||||
|
||||
@@ -117,8 +119,8 @@ Use `?theme=THEME_NAME` parameter like so :-
|
||||
[ocean_dark]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=ocean_dark
|
||||
[city_lights]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=city_lights
|
||||
[github_dark]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=github_dark
|
||||
[github_dark_dimmed]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=github_dark_dimmed
|
||||
[discord_old_blurple]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=discord_old_blurple
|
||||
[aura_dark]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=aura_dark
|
||||
[panda]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=panda
|
||||
[noctis_minimus]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=noctis_minimus
|
||||
[cobalt2]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=cobalt2
|
||||
@@ -128,7 +130,7 @@ Use `?theme=THEME_NAME` parameter like so :-
|
||||
[moltack]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=moltack
|
||||
[codeSTACKr]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=codeSTACKr
|
||||
[rose_pine]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=rose_pine
|
||||
|
||||
[aura_dark]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=aura_dark
|
||||
|
||||
[default_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=default
|
||||
[default_repocard_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=default_repocard
|
||||
@@ -180,8 +182,8 @@ Use `?theme=THEME_NAME` parameter like so :-
|
||||
[ocean_dark_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=ocean_dark
|
||||
[city_lights_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=city_lights
|
||||
[github_dark_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=github_dark
|
||||
[github_dark_dimmed_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=github_dark_dimmed
|
||||
[discord_old_blurple_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=discord_old_blurple
|
||||
[aura_dark_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=aura_dark
|
||||
[panda_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=panda
|
||||
[noctis_minimus_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=noctis_minimus
|
||||
[cobalt2_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=cobalt2
|
||||
@@ -191,7 +193,7 @@ Use `?theme=THEME_NAME` parameter like so :-
|
||||
[moltack_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=moltack
|
||||
[codeSTACKr_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=codeSTACKr
|
||||
[rose_pine_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=rose_pine
|
||||
|
||||
[aura_dark_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=aura_dark
|
||||
|
||||
[add-theme]: https://github.com/anuraghazra/github-readme-stats/edit/master/themes/index.js
|
||||
|
||||
|
||||
@@ -18,6 +18,27 @@ export const themes = {
|
||||
text_color: "417E87",
|
||||
bg_color: "ffffff00",
|
||||
},
|
||||
shadow_red: {
|
||||
title_color: "9A0000",
|
||||
text_color: "444",
|
||||
icon_color: "4F0000",
|
||||
border_color: "4F0000",
|
||||
bg_color: "ffffff00",
|
||||
},
|
||||
shadow_green: {
|
||||
title_color: "007A00",
|
||||
text_color: "444",
|
||||
icon_color: "003D00",
|
||||
border_color: "003D00",
|
||||
bg_color: "ffffff00",
|
||||
},
|
||||
shadow_blue: {
|
||||
title_color: "00779A",
|
||||
text_color: "444",
|
||||
icon_color: "004450",
|
||||
border_color: "004490",
|
||||
bg_color: "ffffff00",
|
||||
},
|
||||
dark: {
|
||||
title_color: "fff",
|
||||
icon_color: "79ff97",
|
||||
@@ -300,6 +321,13 @@ export const themes = {
|
||||
text_color: "C3D1D9",
|
||||
bg_color: "0D1117",
|
||||
},
|
||||
github_dark_dimmed: {
|
||||
title_color: "539bf5",
|
||||
icon_color: "539bf5",
|
||||
text_color: "ADBAC7",
|
||||
bg_color: "24292F",
|
||||
border_color: "373E47",
|
||||
},
|
||||
discord_old_blurple: {
|
||||
title_color: "7289DA",
|
||||
icon_color: "7289DA",
|
||||
@@ -374,6 +402,27 @@ export const themes = {
|
||||
border_color: "170F0C",
|
||||
bg_color: "170F0C",
|
||||
},
|
||||
one_dark_pro: {
|
||||
title_color: "61AFEF",
|
||||
text_color: "E5C06E",
|
||||
icon_color: "C678DD",
|
||||
border_color: "3B4048",
|
||||
bg_color: "23272E",
|
||||
},
|
||||
rose: {
|
||||
title_color: "8d192b",
|
||||
text_color: "862931",
|
||||
icon_color: "B71F36",
|
||||
border_color: "e9d8d4",
|
||||
bg_color: "e9d8d4",
|
||||
},
|
||||
holi: {
|
||||
title_color: "5FABEE",
|
||||
text_color: "D6E7FF",
|
||||
icon_color: "5FABEE",
|
||||
border_color: "85A4C0",
|
||||
bg_color: "030314",
|
||||
},
|
||||
};
|
||||
|
||||
export default themes;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"functions": {
|
||||
"api/*.js": {
|
||||
"memory": 128,
|
||||
"maxDuration": 30
|
||||
"maxDuration": 10
|
||||
}
|
||||
},
|
||||
"redirects": [
|
||||
|
||||
Reference in New Issue
Block a user