Compare commits
113
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5076d88a34 | ||
|
|
1f79d503bd | ||
|
|
ef57cc7dcf | ||
|
|
f0a0dfe51c | ||
|
|
7f220bb072 | ||
|
|
57cf490e3e | ||
|
|
6d1b1e220c | ||
|
|
662ae95b02 | ||
|
|
e38df2932c | ||
|
|
64d1fb46c3 | ||
|
|
753f0b9fec | ||
|
|
f50938c908 | ||
|
|
6d5a828542 | ||
|
|
e32ab3f973 | ||
|
|
2812ce0fd3 | ||
|
|
f9b14c9442 | ||
|
|
7b509d3128 | ||
|
|
f355f48bb4 | ||
|
|
4c0518616f | ||
|
|
3a9ded0a09 | ||
|
|
7973805cf7 | ||
|
|
a7d56c3a04 | ||
|
|
13d9da68ff | ||
|
|
9624cf6acd | ||
|
|
7a8510641c | ||
|
|
bd13399e7f | ||
|
|
aa44bd7615 | ||
|
|
c2adcfd6fe | ||
|
|
17c33cd322 | ||
|
|
1c4cb84916 | ||
|
|
d394add560 | ||
|
|
5ed75e11be | ||
|
|
03f55e809e | ||
|
|
63ba6e6601 | ||
|
|
dc3e9a59f2 | ||
|
|
8284be105d | ||
|
|
6c16020c53 | ||
|
|
8a8ff5b390 | ||
|
|
4b01b4187c | ||
|
|
c3de4675c5 | ||
|
|
ad87824f3c | ||
|
|
bc7a9857a9 | ||
|
|
4a8fd03d8d | ||
|
|
7c104cf8c5 | ||
|
|
b50555ad9a | ||
|
|
411d8f1ffa | ||
|
|
938df3f7ea | ||
|
|
92dcc89a3d | ||
|
|
815916b3f1 | ||
|
|
d04d682eb2 | ||
|
|
f78136c533 | ||
|
|
f4f277a8e2 | ||
|
|
a5fa1a0d1b | ||
|
|
fdf445d734 | ||
|
|
9fb28c9112 | ||
|
|
8fc85c68b1 | ||
|
|
dd2c7ed278 | ||
|
|
9a7ed8f192 | ||
|
|
71c63abe3a | ||
|
|
30da0119bf | ||
|
|
2c26329e13 | ||
|
|
b4a9bd4468 | ||
|
|
f5cd85f11f | ||
|
|
c436ed1121 | ||
|
|
f6c901b938 | ||
|
|
e9dc35318b | ||
|
|
253eb39b19 | ||
|
|
06f0021660 | ||
|
|
f27daeb931 | ||
|
|
b6aa462881 | ||
|
|
be79fe4051 | ||
|
|
b039fa16af | ||
|
|
61442cb359 | ||
|
|
431f2a0b5b | ||
|
|
2709d5b434 | ||
|
|
96f89ad2b7 | ||
|
|
7dcb60f576 | ||
|
|
d5f410d4ec | ||
|
|
1c0332352a | ||
|
|
4febc6644a | ||
|
|
6fedd69f86 | ||
|
|
1078c85455 | ||
|
|
9e4d83247e | ||
|
|
597dac2985 | ||
|
|
0ea2668176 | ||
|
|
1091fc7695 | ||
|
|
6d4fbfecfb | ||
|
|
331295139d | ||
|
|
a726e8dbaf | ||
|
|
817f736b4e | ||
|
|
987fb0c30d | ||
|
|
96d37c706f | ||
|
|
3a24ad62b1 | ||
|
|
989079e4fd | ||
|
|
20bddc3a0c | ||
|
|
95f1233ab9 | ||
|
|
1a53fb9a84 | ||
|
|
19bacf780a | ||
|
|
89bb8fea42 | ||
|
|
0bade9be77 | ||
|
|
5fce46d67c | ||
|
|
00ce9d37d2 | ||
|
|
9328a57ea6 | ||
|
|
579ed2bab8 | ||
|
|
429d65a52b | ||
|
|
3d8dea90e2 | ||
|
|
47370adbbb | ||
|
|
2efb399f33 | ||
|
|
dcde5f8205 | ||
|
|
3b3ce41ae4 | ||
|
|
e3519ceb4c | ||
|
|
c297798889 | ||
|
|
2658a8737d |
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: ["https://www.paypal.me/anuraghazra", "https://www.buymeacoffee.com/anuraghazra"] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots / Live demo link (paste the github-readme-stats link as markdown image)**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,35 @@
|
||||
name: Generate Theme Readme
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "themes/index.js"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: "12.x"
|
||||
|
||||
- name: npm install, generate readme
|
||||
run: |
|
||||
npm install
|
||||
npm run theme-readme-gen
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Run Script
|
||||
uses: skx/github-action-tester@master
|
||||
with:
|
||||
script: ./push-theme-readme.sh
|
||||
env:
|
||||
CI: true
|
||||
PERSONAL_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
GH_REPO: ${{ secrets.GH_REPO }}
|
||||
@@ -34,3 +34,6 @@ jobs:
|
||||
run: |
|
||||
npm install
|
||||
npm run test
|
||||
|
||||
- name: Code Coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
|
||||
+4
-1
@@ -1,4 +1,7 @@
|
||||
.vercel
|
||||
.env
|
||||
node_modules
|
||||
package-lock.json
|
||||
package-lock.json
|
||||
*.lock
|
||||
.vscode/
|
||||
coverage
|
||||
|
||||
+2
-1
@@ -1,2 +1,3 @@
|
||||
.env
|
||||
package-lock.json
|
||||
package-lock.json
|
||||
coverage
|
||||
@@ -0,0 +1,75 @@
|
||||
# Contributing to [github-readme-stats](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
|
||||
|
||||
- Reporting a issue
|
||||
- Discussing the current state of the code
|
||||
- Submitting a fix
|
||||
- Proposing new features
|
||||
- Becoming a maintainer
|
||||
|
||||
## All Changes Happen Through Pull Requests
|
||||
|
||||
Pull requests are the best way to propose changes. We actively welcome your pull requests:
|
||||
|
||||
1. Fork the repo and create your branch from `master`.
|
||||
1. If you've added code that should be tested, add some tests' example.
|
||||
1. If you've changed APIs, update the documentation.
|
||||
1. Issue that pull request!
|
||||
|
||||
## Local Development
|
||||
|
||||
To run & test github-readme-stats you need to follow few simple steps :-
|
||||
_(make sure you already have a [vercel](https://vercel.com/) account)_
|
||||
|
||||
1. Install [Vercel CLI](https://vercel.com/download)
|
||||
1. Fork the repository and clone the code to your local machine
|
||||
1. Run the command "vercel" in the root and follow the steps there
|
||||
1. Create a `.env` file in the root of the directory
|
||||
1. In the .env file add a new variable named "PAT" with your [github Personal access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token)
|
||||
1. Run the command "vercel dev" to start a development server at https://localhost:3000
|
||||
|
||||
## Themes Contribution
|
||||
|
||||
Github readme stats supports custom theming and you can also contribute new themes!
|
||||
|
||||
All you need to do is edit [themes/index.js](./themes/index.js) file and add your theme at the end of the file.
|
||||
|
||||
While creating the Pull request to add a new theme **don't forget to add a screenshot of how your theme looks**, you can also test how it looks using custom url parameters like `title_color`, `icon_color`, `bg_color`, `text_color`
|
||||
|
||||
> NOTE: If you are contributing your theme just because you are using it personally, then you can [customize the looks](./readme.md#customization) of your card with URL params instead.
|
||||
|
||||
## Any contributions you make will be under the MIT Software License
|
||||
|
||||
In short, when you submit changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
|
||||
|
||||
## Report issues/bugs using Github's [issues](https://github.com/anuraghazra/github-readme-stats/issues)
|
||||
|
||||
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/anuraghazra/github-readme-stats/issues/new/choose); it's that easy!
|
||||
|
||||
### Bug Reports
|
||||
|
||||
**Great Bug Reports** tend to have:
|
||||
|
||||
- A quick summary and/or background
|
||||
- Steps to reproduce
|
||||
- Be specific!
|
||||
- Share the snapshot, if possible.
|
||||
- Github Readme Stats' live link
|
||||
- What actually happens
|
||||
- What you expected would happen
|
||||
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
|
||||
|
||||
People _love_ thorough bug reports. I'm not even kidding.
|
||||
|
||||
### Feature Request
|
||||
|
||||
**Great Feature Requests** tend to have:
|
||||
|
||||
- A quick idea summary
|
||||
- What & why you wanted to add the specific feature
|
||||
- Additional Context like images, links to resources to implement the feature etc etc.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under its [MIT License](./LICENSE).
|
||||
+33
-7
@@ -1,5 +1,11 @@
|
||||
require("dotenv").config();
|
||||
const { renderError } = require("../src/utils");
|
||||
const {
|
||||
renderError,
|
||||
parseBoolean,
|
||||
parseArray,
|
||||
clampValue,
|
||||
CONSTANTS,
|
||||
} = require("../src/utils");
|
||||
const fetchStats = require("../src/fetchStats");
|
||||
const renderStatsCard = require("../src/renderStatsCard");
|
||||
|
||||
@@ -7,35 +13,55 @@ module.exports = async (req, res) => {
|
||||
const {
|
||||
username,
|
||||
hide,
|
||||
hide_title,
|
||||
hide_border,
|
||||
hide_rank,
|
||||
show_icons,
|
||||
count_private,
|
||||
line_height,
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
cache_seconds,
|
||||
} = req.query;
|
||||
let stats;
|
||||
|
||||
res.setHeader("Content-Type", "image/svg+xml");
|
||||
|
||||
try {
|
||||
stats = await fetchStats(username);
|
||||
stats = await fetchStats(username, parseBoolean(count_private));
|
||||
} catch (err) {
|
||||
return res.send(renderError(err.message));
|
||||
return res.send(
|
||||
renderError(
|
||||
err.message,
|
||||
"Make sure the provided username is not an organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const cacheSeconds = clampValue(
|
||||
parseInt(cache_seconds || CONSTANTS.THIRTY_MINUTES, 10),
|
||||
CONSTANTS.THIRTY_MINUTES,
|
||||
CONSTANTS.ONE_DAY
|
||||
);
|
||||
|
||||
res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`);
|
||||
|
||||
res.send(
|
||||
renderStatsCard(stats, {
|
||||
hide: JSON.parse(hide || "[]"),
|
||||
show_icons,
|
||||
hide_border,
|
||||
hide_rank,
|
||||
hide: parseArray(hide),
|
||||
show_icons: parseBoolean(show_icons),
|
||||
hide_title: parseBoolean(hide_title),
|
||||
hide_border: parseBoolean(hide_border),
|
||||
hide_rank: parseBoolean(hide_rank),
|
||||
line_height,
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
+35
-2
@@ -1,5 +1,11 @@
|
||||
require("dotenv").config();
|
||||
const { renderError } = require("../src/utils");
|
||||
const {
|
||||
renderError,
|
||||
parseBoolean,
|
||||
clampValue,
|
||||
CONSTANTS,
|
||||
logger,
|
||||
} = require("../src/utils");
|
||||
const fetchRepo = require("../src/fetchRepo");
|
||||
const renderRepoCard = require("../src/renderRepoCard");
|
||||
|
||||
@@ -11,24 +17,51 @@ module.exports = async (req, res) => {
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
show_owner,
|
||||
cache_seconds,
|
||||
} = req.query;
|
||||
|
||||
let repoData;
|
||||
|
||||
res.setHeader("Content-Type", "image/svg+xml");
|
||||
|
||||
try {
|
||||
repoData = await fetchRepo(username, repo);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
logger.error(err);
|
||||
return res.send(renderError(err.message));
|
||||
}
|
||||
|
||||
let cacheSeconds = clampValue(
|
||||
parseInt(cache_seconds || CONSTANTS.THIRTY_MINUTES, 10),
|
||||
CONSTANTS.THIRTY_MINUTES,
|
||||
CONSTANTS.ONE_DAY
|
||||
);
|
||||
|
||||
/*
|
||||
if star count & fork count is over 1k then we are kFormating the text
|
||||
and if both are zero we are not showing the stats
|
||||
so we can just make the cache longer, since there is no need to frequent updates
|
||||
*/
|
||||
const stars = repoData.stargazers.totalCount;
|
||||
const forks = repoData.forkCount;
|
||||
const isBothOver1K = stars > 1000 && forks > 1000;
|
||||
const isBothUnder1 = stars < 1 && forks < 1;
|
||||
if (!cache_seconds && (isBothOver1K || isBothUnder1)) {
|
||||
cacheSeconds = CONSTANTS.TWO_HOURS;
|
||||
}
|
||||
|
||||
res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`);
|
||||
|
||||
res.send(
|
||||
renderRepoCard(repoData, {
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
show_owner: parseBoolean(show_owner),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
require("dotenv").config();
|
||||
const {
|
||||
renderError,
|
||||
clampValue,
|
||||
parseBoolean,
|
||||
parseArray,
|
||||
CONSTANTS,
|
||||
} = require("../src/utils");
|
||||
const fetchTopLanguages = require("../src/fetchTopLanguages");
|
||||
const renderTopLanguages = require("../src/renderTopLanguages");
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
const {
|
||||
username,
|
||||
hide,
|
||||
hide_title,
|
||||
card_width,
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
cache_seconds,
|
||||
} = req.query;
|
||||
let topLangs;
|
||||
|
||||
res.setHeader("Content-Type", "image/svg+xml");
|
||||
|
||||
try {
|
||||
topLangs = await fetchTopLanguages(username);
|
||||
} catch (err) {
|
||||
return res.send(renderError(err.message));
|
||||
}
|
||||
|
||||
const cacheSeconds = clampValue(
|
||||
parseInt(cache_seconds || CONSTANTS.THIRTY_MINUTES, 10),
|
||||
CONSTANTS.THIRTY_MINUTES,
|
||||
CONSTANTS.ONE_DAY
|
||||
);
|
||||
|
||||
res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`);
|
||||
|
||||
res.send(
|
||||
renderTopLanguages(topLangs, {
|
||||
theme,
|
||||
hide_title: parseBoolean(hide_title),
|
||||
card_width: parseInt(card_width, 10),
|
||||
hide: parseArray(hide),
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
})
|
||||
);
|
||||
};
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
codecov:
|
||||
require_ci_to_pass: yes
|
||||
|
||||
coverage:
|
||||
precision: 2
|
||||
round: down
|
||||
range: "70...100"
|
||||
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
threshold: 5
|
||||
patch: false
|
||||
+11
-3
@@ -4,8 +4,9 @@
|
||||
"description": "Dynamically generate stats for your github readmes",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
"test": "jest --coverage",
|
||||
"test:watch": "jest --watch",
|
||||
"theme-readme-gen": "node scripts/generate-theme-doc"
|
||||
},
|
||||
"author": "Anurag Hazra",
|
||||
"license": "MIT",
|
||||
@@ -15,9 +16,16 @@
|
||||
"axios": "^0.19.2",
|
||||
"axios-mock-adapter": "^1.18.1",
|
||||
"css-to-object": "^1.1.0",
|
||||
"husky": "^4.2.5",
|
||||
"jest": "^26.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^8.2.0"
|
||||
"dotenv": "^8.2.0",
|
||||
"emoji-name-map": "^1.2.8"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
<p align="center">
|
||||
<h2 align="center">Github Readme Stats</h2>
|
||||
<img width="100px" src="https://res.cloudinary.com/anuraghazra/image/upload/v1594908242/logo_ccswme.svg" align="center" alt="Github Readme Stats" />
|
||||
<h2 align="center">GitHub Readme Stats</h2>
|
||||
<p align="center">Get dynamically generated GitHub stats on your readmes!</p>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/actions">
|
||||
<img alt="Tests Passing" src="https://github.com/anuraghazra/github-readme-stats/workflows/Test/badge.svg" />
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/anuraghazra/github-readme-stats">
|
||||
<img src="https://codecov.io/gh/anuraghazra/github-readme-stats/branch/master/graph/badge.svg" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues">
|
||||
<img alt="Issues" src="https://img.shields.io/github/issues/anuraghazra/github-readme-stats?color=0088ff" />
|
||||
</a>
|
||||
@@ -17,101 +22,117 @@
|
||||
<p align="center">
|
||||
<a href="#demo">View Demo</a>
|
||||
·
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues">Report Bug</a>
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues/new/choose">Report Bug</a>
|
||||
·
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues">Request Feature</a>
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues/new/choose">Request Feature</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="readme_cn.md">简体中文</a>
|
||||
·
|
||||
<a href="readme_es.md">Español</a>
|
||||
</p>
|
||||
</p>
|
||||
<p align="center">Loved the project? Please consider <a href="https://www.paypal.me/anuraghazra">donating</a> to help it improve!
|
||||
|
||||
# Features
|
||||
|
||||
- [Github Stats Card](#github-stats-card)
|
||||
- [Github Extra Pins](#github-extra-pins)
|
||||
- [GitHub Stats Card](#github-stats-card)
|
||||
- [GitHub Extra Pins](#github-extra-pins)
|
||||
- [Top Languages Card](#top-languages-card)
|
||||
- [Themes](#themes)
|
||||
- [Customization](#customization)
|
||||
- [Deploy Yourself](#deploy-on-your-own-vercel-instance)
|
||||
|
||||
# Github Stats Card
|
||||
# GitHub Stats Card
|
||||
|
||||
Copy paste this into your markdown content, and that's it. Simple!
|
||||
|
||||
change the `?username=` value to your GitHubs's username
|
||||
Change the `?username=` value to your GitHub's username.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
_Note: Ranks are calculated based on users stats, see [src/calculateRank.js](./src/calculateRank.js)_
|
||||
_Note: Ranks are calculated based on user's stats, see [src/calculateRank.js](./src/calculateRank.js)_
|
||||
|
||||
### Hiding individual stats
|
||||
|
||||
To hide any specific stats, you can pass a query parameter `?hide=` with an array of items, you wanna hide.
|
||||
To hide any specific stats, you can pass a query parameter `?hide=` with comma seperated values.
|
||||
|
||||
> Options: `&hide=["stars","prs","issues","contribs"]`
|
||||
> Options: `&hide=stars,commits,prs,issues,contribs`
|
||||
|
||||
```md
|
||||

|
||||

|
||||
```
|
||||
|
||||
### Adding private contributions count to total commits count
|
||||
|
||||
You can add the count of all your private contributions to the total commits count by using the query parameter `?count_private=true`.
|
||||
|
||||
_Note: If you are deploying this project yourself, the private contributions will be counted by default otherwise you need to chose to share your private contribution counts._
|
||||
|
||||
> Options: `&count_private=true`
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### 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
|
||||

|
||||
```
|
||||
|
||||
Other options:
|
||||
### Themes
|
||||
|
||||
- `&hide_border=true` hide the border box if you don't like it :D.
|
||||
- `&line_height=30` control the line-height between text.
|
||||
- `&hide_rank=true` hides the ranking
|
||||
With inbuilt themes you can customize the look of the card without doing any [manual customization](#customization).
|
||||
|
||||
Use `?theme=THEME_NAME` parameter like so :-
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
#### All inbuilt themes :-
|
||||
|
||||
dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula
|
||||
|
||||
<img src="https://res.cloudinary.com/anuraghazra/image/upload/v1595174536/grs-themes_l4ynja.png" alt="Github Readme Stat Themes" width="600px"/>
|
||||
|
||||
You can look at a preview for [all available themes](./themes/README.md) or checkout the [theme config file](./themes/index.js) & **you can also contribute new themes** if you like :D
|
||||
|
||||
### Customization
|
||||
|
||||
You can customize the appearance of your `Stats Card` or `Repo Card` however you want with url params.
|
||||
You can customize the appearance of your `Stats Card` or `Repo Card` however you want with URL params.
|
||||
|
||||
Customization Options:
|
||||
|
||||
| Option | type | Stats Card (default) | Repo Card (default) |
|
||||
| ----------- | --------- | ---------------------- | ---------------------- |
|
||||
| title_color | hex color | #2f80ed | #2f80ed |
|
||||
| text_color | hex color | #333 | #333 |
|
||||
| icon_color | hex color | #4c71f2 | #586069 |
|
||||
| bg_color | hex color | rgba(255, 255, 255, 0) | rgba(255, 255, 255, 0) |
|
||||
| Option | type | description | Stats Card (default) | Repo Card (default) | Top Lang Card (default) |
|
||||
| ------------- | --------- | ------------------------------------------- | -------------------- | ------------------- | ----------------------- |
|
||||
| title_color | hex color | title color | 2f80ed | 2f80ed | 2f80ed |
|
||||
| text_color | hex color | body color | 333 | 333 | 333 |
|
||||
| icon_color | hex color | icon color | 4c71f2 | 586069 | 586069 |
|
||||
| bg_color | hex color | card bg color | FFFEFE | FFFEFE | FFFEFE |
|
||||
| line_height | number | control the line-height between text | 30 | N/A | N/A |
|
||||
| hide | CSV | hides the items specified | undefined | N/A | undefined |
|
||||
| hide_rank | boolean | hides the ranking | false | N/A | N/A |
|
||||
| hide_title | boolean | hides the stats title | false | N/A | false |
|
||||
| hide_border | boolean | hides the stats card border | false | N/A | N/A |
|
||||
| show_owner | boolean | shows owner name in repo card | N/A | false | N/A |
|
||||
| show_icons | boolean | shows icons | false | N/A | N/A |
|
||||
| theme | string | sets inbuilt theme | 'default' | 'default_repocard' | 'default |
|
||||
| cache_seconds | number | manually set custom cache control | 1800 | 1800 | '1800' |
|
||||
| count_private | boolean | counts private contributions too if enabled | false | N/A | N/A |
|
||||
|
||||
- You can also customize the cards to be compatible with dark mode
|
||||
> Note on cache: Repo cards have default cache of 30mins (1800 seconds) if the fork count & star count is less than 1k otherwise it's 2hours (7200). Also note that cache is clamped to minimum of 30min and maximum of 24hours
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
# GitHub Extra Pins
|
||||
|
||||
### Demo
|
||||
GitHub extra pins allow you to pin more than 6 repositories in your profile using a GitHub readme profile.
|
||||
|
||||
- Default
|
||||
|
||||

|
||||
|
||||
- Hiding specific stats
|
||||
|
||||

|
||||
|
||||
- Showing icons
|
||||
|
||||

|
||||
|
||||
- Customizing stats card
|
||||
|
||||

|
||||
|
||||
- Customizing repo card
|
||||
|
||||

|
||||
|
||||
# Github Extra Pins
|
||||
|
||||
Github extra pins allow you to pin more than 6 repositories in your profile using a GitHub readme profile.
|
||||
|
||||
Yey! you are no longer limited to 6 pinned repositories.
|
||||
Yey! You are no longer limited to 6 pinned repositories.
|
||||
|
||||
### Usage
|
||||
|
||||
@@ -127,9 +148,77 @@ Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
Use [show_owner](#customization) variable to include the repo's owner username
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
# Top Languages Card
|
||||
|
||||
Top languages card shows github user's top langauges which has been mostly used.
|
||||
|
||||
_NOTE: Top languages does not indicate my skill level or something like that, it's a github metric of which languages i have the most code on github, it's a new feature of github-readme-stats_
|
||||
|
||||
### Usage
|
||||
|
||||
Copy-paste this code into your readme and change the links.
|
||||
|
||||
Endpoint: `api/top-langs?username=anuraghazra`
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Hide individual languages
|
||||
|
||||
You can use `?hide=language1,language2` parameter to hide individual languages.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Demo
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
---
|
||||
|
||||
### All Demos
|
||||
|
||||
- Default
|
||||
|
||||

|
||||
|
||||
- Hiding specific stats
|
||||
|
||||

|
||||
|
||||
- Showing icons
|
||||
|
||||

|
||||
|
||||
- Themes
|
||||
|
||||
Choose from any of the [default themes](#themes)
|
||||
|
||||

|
||||
|
||||
- Customizing stats card
|
||||
|
||||

|
||||
|
||||
- Customizing repo card
|
||||
|
||||

|
||||
|
||||
- Top languages
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
---
|
||||
|
||||
### Quick Tip (Align The Repo Cards)
|
||||
|
||||
You usually won't be able to layout the images side by side to do that you can use this approach
|
||||
You usually won't be able to layout the images side by side. To do that you can use this approach:
|
||||
|
||||
```md
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats">
|
||||
@@ -140,15 +229,50 @@ You usually won't be able to layout the images side by side to do that you can u
|
||||
</a>
|
||||
```
|
||||
|
||||
## Deploy on your own vercel instance
|
||||
## Deploy on your own Vercel instance
|
||||
|
||||
Since Github API only allows 5k requests per hour it is possible that my `https://github-readme-stats.vercel.app/api` could hit the rate limiter thats why if you want to host it on your own vercel server then you don't have to worry about anything. click on the deploy button to get started
|
||||
Since the GitHub API only allows 5k requests per hour, it is possible that my `https://github-readme-stats.vercel.app/api` could hit the rate limiter. If you host it on your own Vercel server, then you don't have to worry about anything. Click on the deploy button to get started!
|
||||
|
||||
NOTE: Since [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) we should be able to handle more than 5k requests and have no issues with downtime :D
|
||||
|
||||
[](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
Make sure to add your own `GITHUB_TOKEN` in `Environment Variables`
|
||||
[Github Docs: Creating a github personal token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token)
|
||||
<details>
|
||||
<summary>Guide on setting up Vercel</summary>
|
||||
|
||||
1. Go to [vercel.com](https://vercel.com/)
|
||||
1. Click on `Log in`
|
||||

|
||||
1. Sign in with GitHub by pressing `Continue with GitHub`
|
||||

|
||||
1. Sign into GitHub and allow access to all repositories, if prompted
|
||||
1. Fork this repo
|
||||
1. Go back to your [Vercel dashboard](https://vercel.com/dashboard)
|
||||
1. Select `Import Project`
|
||||

|
||||
1. Select `Import Git Repository`
|
||||

|
||||
1. Select root and keep everything as is, just add your environment variable named PAT_1 (as shown), which will contain a personal access token (PAT), which you can easily create [here](https://github.com/settings/tokens/new) (leave everything as is, just name it something, it can be anything you want)
|
||||

|
||||
1. Click deploy, and you're good to go. See your domains to use the API!
|
||||
|
||||
</details>
|
||||
|
||||
## :sparkling_heart: Support the project
|
||||
|
||||
I open-source almost everything I can, and I try to reply to everyone needing help using these projects. Obviously,
|
||||
this takes time. You can use this service for free.
|
||||
|
||||
However, if you are using this project and happy with it or just want to encourage me to continue creating stuff, there are few ways you can do it :-
|
||||
|
||||
- Giving proper credit when you use github-readme-stats on your readme, linking back to it :D
|
||||
- Starring and sharing the project :rocket:
|
||||
- [](https://www.paypal.me/anuraghazra) - You can make one-time donations via PayPal. I'll probably buy a ~~coffee~~ tea. :tea:
|
||||
|
||||
Thanks! :heart:
|
||||
|
||||
---
|
||||
|
||||
Contributions are welcomed! <3
|
||||
|
||||
Made with :heart: and javascript.
|
||||
Made with :heart: and JavaScript.
|
||||
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
<p align="center">
|
||||
<img width="100px" src="https://res.cloudinary.com/anuraghazra/image/upload/v1594908242/logo_ccswme.svg" align="center" alt="Github Readme Stats" />
|
||||
<h2 align="center">GitHub Readme Stats</h2>
|
||||
<p align="center">在你的 README 中 获取动态生成的 GitHub 统计信息!</p>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/actions">
|
||||
<img alt="Tests Passing" src="https://github.com/anuraghazra/github-readme-stats/workflows/Test/badge.svg" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues">
|
||||
<img alt="Issues" src="https://img.shields.io/github/issues/anuraghazra/github-readme-stats?color=0088ff" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/pulls">
|
||||
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/anuraghazra/github-readme-stats?color=0088ff" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#demo">查看 Demo</a>
|
||||
·
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues/new/choose">报告 bug</a>
|
||||
·
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues/new/choose">请求增加功能</a>
|
||||
</p>
|
||||
</p>
|
||||
<p align="center">喜欢这个项目?请考虑<a href="https://www.paypal.me/anuraghazra">捐赠</a>来帮助它完善!
|
||||
|
||||
# 特性
|
||||
|
||||
- [GitHub 统计卡片](#GitHub-统计卡片)
|
||||
- [GitHub 更多置顶](#GitHub-更多置顶)
|
||||
- [热门语言卡片](#热门语言卡片)
|
||||
- [主题](#主题)
|
||||
- [自定义](#自定义)
|
||||
- [自己部署](#自己部署)
|
||||
|
||||
# GitHub 统计卡片
|
||||
|
||||
将这行代码复制到你的 markdown 文件中,简单如此!
|
||||
更改 `?username=` 的值为你的 GitHub 用户名。
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
_Note: 排名基于用户的统计信息计算得出,详见 [src/calculateRank.js](./src/calculateRank.js)_
|
||||
|
||||
### 隐藏个人统计信息
|
||||
|
||||
想要隐藏某项统计信息,你可以调用参数 `?hide=`,并调用 你想要隐藏的项目 组成的数组。
|
||||
|
||||
> 选项:`&hide=["stars","prs","issues","contribs"]`
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### 显示图标
|
||||
|
||||
想要显示图标,你可以调用 `show_icons=true` 参数,如下:
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### 主题
|
||||
|
||||
你可以通过现有的主题进行卡片个性化,省去[手动自定义](#自定义)的麻烦。
|
||||
|
||||
调用 `?theme=THEME_NAME` 参数,如下:
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
#### 所有现有主题
|
||||
|
||||
dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula
|
||||
|
||||
<img src="https://res.cloudinary.com/anuraghazra/image/upload/v1595174536/grs-themes_l4ynja.png" alt="Github Readme Stat Themes" width="600px"/>
|
||||
|
||||
在 [theme config 文件](./themes/index.js) 中查看更多主题,或者 **贡献新的主题** :D
|
||||
|
||||
### 自定义
|
||||
|
||||
你可以通过使用 URL 参数的方式,为你的 `Stats Card` 或 `Repo Card` 自定义样式。
|
||||
|
||||
自定义选项:
|
||||
|
||||
| Option | type | description | Stats Card (default) | Repo Card (default) |
|
||||
| ----------- | --------- | ------------------------------------ | -------------------- | ------------------- |
|
||||
| title_color | hex color | 标题颜色 | 2f80ed | 2f80ed |
|
||||
| text_color | hex color | 字体颜色 | 333 | 333 |
|
||||
| icon_color | hex color | 图标颜色 | 4c71f2 | 586069 |
|
||||
| bg_color | hex color | 卡片背景颜色 | FFFEFE | FFFEFE |
|
||||
| line_height | number | 文字行高 | 30 | N/A |
|
||||
| hide_rank | boolean | 隐藏评分等级 | false | N/A |
|
||||
| hide_title | boolean | 隐藏卡片标题 | false | N/A |
|
||||
| hide_border | boolean | 隐藏卡片边框 | false | N/A |
|
||||
| show_owner | boolean | 显示 Repo 卡片所属账户用户名 | N/A | false |
|
||||
| show_icons | boolean | 显示图标 | false | N/A |
|
||||
| theme | string | 设置主题 | 'default' | 'default_repocard' |
|
||||
|
||||
---
|
||||
|
||||
### Demo
|
||||
|
||||
- 默认
|
||||
|
||||

|
||||
|
||||
- 隐藏特定数据
|
||||
|
||||

|
||||
|
||||
- 显示图标
|
||||
|
||||

|
||||
|
||||
- 主题
|
||||
|
||||
从 [默认主题](#主题) 中进行选择
|
||||
|
||||

|
||||
|
||||
- 自定义统计卡片
|
||||
|
||||

|
||||
|
||||
- 自定义代码库卡片
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
# GitHub 更多置顶
|
||||
|
||||
GitHub 更多置顶 让你使用 README Profile,在个人页面中置顶多于 6 个 repo 。
|
||||
|
||||
这波可以!你再也不用受限于最多 6 个置顶了。
|
||||
|
||||
### 使用细则
|
||||
|
||||
复制粘贴这段代码到你的 README 文件中,并更改链接。
|
||||
|
||||
Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Demo
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
使用 [show_owner](#自定义) 变量将 Repo 所属账户的用户名包含在内。
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
# 热门语言卡片
|
||||
|
||||
热门语言卡片显示了Github用户常用的编程语言。
|
||||
|
||||
*注意:热门语言并不表示我的技能水平或类似的水平,它是用户在Github上拥有最多代码的一项指标,它是github-readme-stats的新功能*
|
||||
|
||||
### 使用细则
|
||||
|
||||
将此代码复制粘贴到您的`README.md`文件中,并改变链接。
|
||||
|
||||
Endpoint: `api/top-langs?username=anuraghazra`
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### 隐藏特定语言
|
||||
|
||||
可以使用`?hide=语言1,语言2`参数来隐藏指定的语言。
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Demo
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
---
|
||||
|
||||
### 全部Demo
|
||||
|
||||
- 默认
|
||||
|
||||

|
||||
|
||||
- 隐藏特定数据
|
||||
|
||||

|
||||
|
||||
- 显示图标
|
||||
|
||||

|
||||
|
||||
- 主题
|
||||
|
||||
从 [默认主题](#主题) 中进行选择
|
||||
|
||||

|
||||
|
||||
- 自定义统计卡片
|
||||
|
||||

|
||||
|
||||
- 自定义代码库卡片
|
||||
|
||||

|
||||
|
||||
- 热门语言
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
---
|
||||
|
||||
### 提示 (对齐 Repo 卡片)
|
||||
|
||||
你通常无法将图片靠边显示。为此,您可以使用以下方法:
|
||||
|
||||
```md
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats">
|
||||
<img align="left" src="https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/convoychat">
|
||||
<img align="left" src="https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=convoychat" />
|
||||
</a>
|
||||
```
|
||||
|
||||
## 自己部署
|
||||
|
||||
因为 GitHub 的 API 每个小时只允许 5 千次请求,我的 `https://github-readme-stats.vercel.app/api` 很有可能会触发限制
|
||||
如果你将其托管在自己的 Vercel 服务器上,那么你就不必为此担心。点击 deploy 按钮来开始你的部署!
|
||||
|
||||
注意: 从 [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) 开始,我们应该能够处理超过 5千 的请求,并且不会出现宕机问题 :D
|
||||
|
||||
[](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
<details>
|
||||
<summary>设置 Vercel 的指导</summary>
|
||||
|
||||
1. 前往 [vercel.com](https://vercel.com/)
|
||||
1. 点击 `Log in`
|
||||

|
||||
1. 点击 `Continue with GitHub` 通过 GitHub 进行登录
|
||||

|
||||
1. 登录 GitHub 并允许访问所有存储库(如果系统这样提示)
|
||||
1. Fork 这个仓库
|
||||
1. 返回到你的 [Vercel dashboard](https://vercel.com/dashboard)
|
||||
1. 选择 `Import Project`
|
||||

|
||||
1. 选择 `Import Git Repository`
|
||||

|
||||
1. 选择 root 并将所有内容保持不变,并且只需添加名为 PAT_1 的环境变量(如图所示),其中将包含一个个人访问令牌(PAT),你可以在[这里](https://github.com/settings/tokens/new)轻松创建(保留默认,并且只需要命名下,名字随便)
|
||||

|
||||
1. 点击 deploy,这就完成了,查看你的域名就可使用 API 了!
|
||||
|
||||
</details>
|
||||
|
||||
## :sparkling_heart: 支持这个项目
|
||||
|
||||
我尽己所能地进行开源,并且我尽量回复每个在使用项目时需要帮助的人。很明显,这需要时间,但你可以免费享受这些。
|
||||
|
||||
然而, 如果你正在使用这个项目并感觉良好,或只是想要支持我继续开发,你可以通过如下方式:
|
||||
|
||||
- 在你的 README 中使用 github-readme-stats 时,链接指向会这里 :D
|
||||
- Star 并 分享这个项目 :rocket:
|
||||
- [](https://www.paypal.me/anuraghazra) - 你可以通过 PayPal 一次性捐款. 我多半会买一杯 ~~咖啡~~ 茶。:tea:
|
||||
|
||||
谢谢! :heart:
|
||||
|
||||
---
|
||||
|
||||
欢迎贡献! <3
|
||||
|
||||
用 :heart: 发电,用 JavaScript 制作。
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
<p align="center">
|
||||
<img width="100px" src="https://res.cloudinary.com/anuraghazra/image/upload/v1594908242/logo_ccswme.svg" align="center" alt="Github Readme Stats" />
|
||||
<h2 align="center">GitHub Readme Stats</h2>
|
||||
<p align="center">¡Obtén tus estadísticas de GitHub generadas dinámicamente en tu README!</p>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/actions">
|
||||
<img alt="Tests Passing" src="https://github.com/anuraghazra/github-readme-stats/workflows/Test/badge.svg" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues">
|
||||
<img alt="Issues" src="https://img.shields.io/github/issues/anuraghazra/github-readme-stats?color=0088ff" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/pulls">
|
||||
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/anuraghazra/github-readme-stats?color=0088ff" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#ejemplo">Ve un ejemplo</a>
|
||||
·
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues/new/choose">Reporta un bug</a>
|
||||
·
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues/new/choose">Solicita una mejora</a>
|
||||
</p>
|
||||
</p>
|
||||
<p align="center">¿Te gusta este proyecto? ¡Por favor considera <a href="https://www.paypal.me/anuraghazra">donar</a> para ayudar a mejorarlo!
|
||||
|
||||
# Características
|
||||
|
||||
- [Tarjeta de estadísticas de GitHub](#tarjeta-de-estadísticas-de-github)
|
||||
- [Pins extra de GitHub](#pins-extra-de-github)
|
||||
- [Temas](#temas)
|
||||
- [Personalización](#personalización)
|
||||
- [Despliega por tu cuenta](#despliega-tu-propia-instancia-de-vercel)
|
||||
|
||||
# Tarjeta de estadísticas de GitHub
|
||||
|
||||
Copia y pega esto en el contenido de tu README.md y listo. ¡Simple!
|
||||
|
||||
Cambia el valor `?username=` al nombre de tu usuario de GitHub.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
_Nota: las clasificaciones se calculan basándose en las estadísticas del usuario. Ve [src/calculateRank.js](./src/calculateRank.js)._
|
||||
|
||||
### Ocultar estadísticas individualmente
|
||||
|
||||
Para ocultar alguna estadística específica, puedes utilizar el parámetro `?hide=` con un arreglo de items que quieras ocultar.
|
||||
|
||||
> Opciones: `&hide=["stars","commits","prs","issues","contribs"]`
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### Mostrar íconos
|
||||
|
||||
Para habilitar los íconos, puedes utilizar `show_icons=true` como parámetro, de esta manera:
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### Temas
|
||||
|
||||
Puedes personalizar el aspecto de la tarjeta sin realizar ninguna [personalización manual](#personalización) con los temas incorporados.
|
||||
|
||||
Utiliza el parámetro `?theme=THEME_NAME`, de esta manera:
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
#### Todos los temas incorporados
|
||||
|
||||
dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula
|
||||
|
||||
<img src="https://res.cloudinary.com/anuraghazra/image/upload/v1595174536/grs-themes_l4ynja.png" alt="Github Readme Stat Themes" width="600px"/>
|
||||
|
||||
Puedes ver una vista previa de [todos los temas disponibles](./themes/README.md) o ver el [archivo de configuración](./themes/index.js) del tema y también **puedes contribuir con nuevos temas** si lo deseas: D
|
||||
|
||||
### Personalización
|
||||
|
||||
Puedes personalizar el aspecto de tu `Stats Card` o `Repo Card` de la manera que desees con los parámetros URL.
|
||||
|
||||
Opciones de personalización:
|
||||
|
||||
| Option | type | description | Stats Card (default) | Repo Card (default) |
|
||||
| ----------- | --------- | ----------------------------- | -------------------- | ------------------- |
|
||||
| title_color | hex color | color del título | 2f80ed | 2f80ed |
|
||||
| text_color | hex color | color del contenido | 333 | 333 |
|
||||
| icon_color | hex color | color del ícono | 4c71f2 | 586069 |
|
||||
| bg_color | hex color | color de fondo | FFFEFE | FFFEFE |
|
||||
| line_height | number | controla el line_height | 30 | N/A |
|
||||
| hide_rank | boolean | oculta la clasificación | false | N/A |
|
||||
| hide_title | boolean | oculta el título | false | N/A |
|
||||
| hide_border | boolean | oculta el borde | false | N/A |
|
||||
| show_owner | boolean | muestra el propietario | N/A | false |
|
||||
| show_icons | boolean | muestra los íconos | false | N/A |
|
||||
| theme | string | establece un tema incorporado | 'default' | 'default_repocard' |
|
||||
|
||||
---
|
||||
|
||||
### Ejemplo
|
||||
|
||||
- Predeterminado
|
||||
|
||||

|
||||
|
||||
- Ocultando estadísticas específicas
|
||||
|
||||

|
||||
|
||||
- Mostrando íconos
|
||||
|
||||

|
||||
|
||||
- Temas
|
||||
|
||||
Elige uno de los [temas predeterminados](#temas)
|
||||
|
||||

|
||||
|
||||
- Personalizando la tarjeta de estadísticas
|
||||
|
||||

|
||||
|
||||
- Personalizando la tarjeta de repositorio
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
# Pins extra de GitHub
|
||||
|
||||
Los pins extra de GitHub te permiten anclar más de 6 repositorios en tu perfil utilizando el archivo README.md.
|
||||
|
||||
¡Bien! Ya no estás limitado a 6 repositorios anclados.
|
||||
|
||||
### Utilización
|
||||
|
||||
Copia y pega este código en tu README.md y cambia los links.
|
||||
|
||||
Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Ejemplo
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
Utiliza la variable [show_owner](#customización) para incluir el nombre de usuario del propietario del repositorio.
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
### Pequeño consejo (alinear las tarjetas de repositorios)
|
||||
|
||||
Usualmente no serías capaz de alinear las imágenes una al lado de otra. Para lograrlo, puedes realizar esto:
|
||||
|
||||
```md
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats">
|
||||
<img align="left" src="https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/convoychat">
|
||||
<img align="left" src="https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=convoychat" />
|
||||
</a>
|
||||
```
|
||||
|
||||
## Despliega tu propia instancia de vercel
|
||||
|
||||
Desde que la API de GitHub permite solo 5 mil peticiones por hora, es posible que mi `https://github-readme-stats.vercel.app/api` pueda llegar al límite. Si lo alojas en tu propio servidor de Vercel, no tendrás que preocuparte de nada. ¡Clickea en el botón "Deploy" para comenzar!
|
||||
|
||||
Nota: debido a esto [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) podríamos manejar más de 5 mil peticiones sin tener ningún problema con el downtime :D
|
||||
|
||||
[](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
<details>
|
||||
<summary>Guía para comenzar en Vercel</summary>
|
||||
|
||||
1. Ve a [vercel.com](https://vercel.com/)
|
||||
1. Clickea en `Log in`
|
||||

|
||||
1. Inicia sesión con GitHub presionando `Continue with GitHub`
|
||||

|
||||
1. Permite el acceso a todos los repositorios (si se te pregunta)
|
||||
1. Haz un Fork de este repositorio
|
||||
1. Dirígete de nuevo a tu [Vercel dashboard](https://vercel.com/dashboard)
|
||||
1. Selecciona `Import Project`
|
||||

|
||||
1. Selecciona `Import Git Repository`
|
||||

|
||||
1. Selecciona "root" y matén todo como está, simplemente añade tu variable de entorno llamada PAT_1 (como se muestra), la cual contendrá un token de acceso personal (PAT), el cual puedes crear fácilmente [aquí](https://github.com/settings/tokens/new) (mantén todo como está, simplemente asígnale un nombre, puede ser cualquiera que desees)
|
||||

|
||||
1. Clickea "Deploy" y ya está listo. ¡Ve tus dominios para usar la API!
|
||||
|
||||
</details>
|
||||
|
||||
## :sparkling_heart: Apoya al proyecto
|
||||
|
||||
Casi todos mis proyectos son código-abierto e intento responder a todos los usuarios que necesiten ayuda con alguno de estos proyectos, Obviamente,
|
||||
esto toma tiempo. Puedes usar este servicio gratis.
|
||||
|
||||
No obstante, si estás utilizando este proyecto y estás feliz con él o simplemente quieres animarme a que siga creando cosas, aquí tienes algunas maneras de hacerlo:
|
||||
|
||||
- Darme créditos cuando estés utilizando github-readme-stats en tu README, añadiendo un link a este repositorio :D
|
||||
- Dándole una estrella (starring) y compartiendo el proyecto :rocket:
|
||||
- [](https://www.paypal.me/anuraghazra) - Puedes hacerme una única donación a través de PayPal. Probablemente me compraré un ~~café~~ té. :tea:
|
||||
|
||||
¡Gracias! :heart:
|
||||
|
||||
---
|
||||
|
||||
¡Las contribuciones son bienvenidas! <3
|
||||
|
||||
Hecho con :heart: y JavaScript.
|
||||
@@ -0,0 +1,115 @@
|
||||
const theme = require("../themes/index");
|
||||
const fs = require("fs");
|
||||
|
||||
const TARGET_FILE = "./themes/README.md";
|
||||
const REPO_CARD_LINKS_FLAG = "<!-- REPO_CARD_LINKS -->";
|
||||
const STAT_CARD_LINKS_FLAG = "<!-- STATS_CARD_LINKS -->";
|
||||
|
||||
const STAT_CARD_TABLE_FLAG = "<!-- STATS_CARD_TABLE -->";
|
||||
const REPO_CARD_TABLE_FLAG = "<!-- REPO_CARD_TABLE -->";
|
||||
|
||||
const THEME_TEMPLATE = `## Available Themes
|
||||
|
||||
<!-- DO NOT EDIT THIS FILE DIRECTLY -->
|
||||
|
||||
With inbuilt themes you can customize the look of the card without doing any manual customization.
|
||||
|
||||
Use \`?theme=THEME_NAME\` parameter like so :-
|
||||
|
||||
\`\`\`md
|
||||

|
||||
\`\`\`
|
||||
|
||||
## Stats
|
||||
|
||||
> These themes work both for the Stats Card and Repo Card.
|
||||
|
||||
| | | |
|
||||
| :--: | :--: | :--: |
|
||||
${STAT_CARD_TABLE_FLAG}
|
||||
|
||||
## Repo Card
|
||||
|
||||
> These themes work both for the Stats Card and Repo Card.
|
||||
|
||||
| | | |
|
||||
| :--: | :--: | :--: |
|
||||
${REPO_CARD_TABLE_FLAG}
|
||||
|
||||
${STAT_CARD_LINKS_FLAG}
|
||||
|
||||
${REPO_CARD_LINKS_FLAG}
|
||||
|
||||
|
||||
[add-theme]: https://github.com/anuraghazra/github-readme-stats/edit/master/themes/index.js
|
||||
|
||||
Wanted to add a new theme? Consider reading the [contribution guidelines](../CONTRIBUTING.md#themes-contribution) :D
|
||||
`;
|
||||
|
||||
const createRepoMdLink = (theme) => {
|
||||
return `\n[${theme}_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=${theme}`;
|
||||
};
|
||||
const createStatMdLink = (theme) => {
|
||||
return `\n[${theme}]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=${theme}`;
|
||||
};
|
||||
|
||||
const generateLinks = (fn) => {
|
||||
return Object.keys(theme)
|
||||
.map((name) => fn(name))
|
||||
.join("");
|
||||
};
|
||||
|
||||
const createTableItem = ({ link, label, isRepoCard }) => {
|
||||
if (!link || !label) return "";
|
||||
return `\`${label}\` ![${link}][${link}${isRepoCard ? "_repo" : ""}]`;
|
||||
};
|
||||
const generateTable = ({ isRepoCard }) => {
|
||||
const rows = [];
|
||||
const themes = Object.keys(theme).filter(
|
||||
(name) => name !== (!isRepoCard ? "default_repocard" : "default")
|
||||
);
|
||||
|
||||
for (let i = 0; i < themes.length; i += 3) {
|
||||
const one = themes[i];
|
||||
const two = themes[i + 1];
|
||||
const three = themes[i + 2];
|
||||
|
||||
let tableItem1 = createTableItem({ link: one, label: one, isRepoCard });
|
||||
let tableItem2 = createTableItem({ link: two, label: two, isRepoCard });
|
||||
let tableItem3 = createTableItem({ link: three, label: three, isRepoCard });
|
||||
|
||||
if (three === undefined) {
|
||||
tableItem3 = `[Add your theme][add-theme]`;
|
||||
}
|
||||
rows.push(`| ${tableItem1} | ${tableItem2} | ${tableItem3} |`);
|
||||
|
||||
// if it's the last row & the row has no empty space push a new row
|
||||
if (three && i + 3 === themes.length) {
|
||||
rows.push(`| [Add your theme][add-theme] | | |`);
|
||||
}
|
||||
}
|
||||
|
||||
return rows.join("\n");
|
||||
};
|
||||
|
||||
const buildReadme = () => {
|
||||
return THEME_TEMPLATE.split("\n")
|
||||
.map((line) => {
|
||||
if (line.includes(REPO_CARD_LINKS_FLAG)) {
|
||||
return generateLinks(createRepoMdLink);
|
||||
}
|
||||
if (line.includes(STAT_CARD_LINKS_FLAG)) {
|
||||
return generateLinks(createStatMdLink);
|
||||
}
|
||||
if (line.includes(REPO_CARD_TABLE_FLAG)) {
|
||||
return generateTable({ isRepoCard: true });
|
||||
}
|
||||
if (line.includes(STAT_CARD_TABLE_FLAG)) {
|
||||
return generateTable({ isRepoCard: false });
|
||||
}
|
||||
return line;
|
||||
})
|
||||
.join("\n");
|
||||
};
|
||||
|
||||
fs.writeFileSync(TARGET_FILE, buildReadme());
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
set -x
|
||||
set -e
|
||||
|
||||
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 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 remote add origin-$BRANCH_NAME https://${PERSONAL_TOKEN}@github.com/${GH_REPO}.git
|
||||
git push --force --quiet --set-upstream origin-$BRANCH_NAME $BRANCH_NAME
|
||||
+60
-19
@@ -1,3 +1,21 @@
|
||||
// https://stackoverflow.com/a/5263759/10629172
|
||||
function normalcdf(mean, sigma, to) {
|
||||
var z = (to - mean) / Math.sqrt(2 * sigma * sigma);
|
||||
var t = 1 / (1 + 0.3275911 * Math.abs(z));
|
||||
var a1 = 0.254829592;
|
||||
var a2 = -0.284496736;
|
||||
var a3 = 1.421413741;
|
||||
var a4 = -1.453152027;
|
||||
var a5 = 1.061405429;
|
||||
var erf =
|
||||
1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-z * z);
|
||||
var sign = 1;
|
||||
if (z < 0) {
|
||||
sign = -1;
|
||||
}
|
||||
return (1 / 2) * (1 + sign * erf);
|
||||
}
|
||||
|
||||
function calculateRank({
|
||||
totalRepos,
|
||||
totalCommits,
|
||||
@@ -13,12 +31,24 @@ function calculateRank({
|
||||
const STARS_OFFSET = 0.75;
|
||||
const PRS_OFFSET = 0.5;
|
||||
const FOLLOWERS_OFFSET = 0.45;
|
||||
const REPO_OFFSET = 1;
|
||||
|
||||
const FIRST_STEP = 0;
|
||||
const SECOND_STEP = 5;
|
||||
const THIRD_STEP = 20;
|
||||
const FOURTH_STEP = 50;
|
||||
const FIFTH_STEP = 130;
|
||||
const ALL_OFFSETS =
|
||||
CONTRIBS_OFFSET +
|
||||
ISSUES_OFFSET +
|
||||
STARS_OFFSET +
|
||||
PRS_OFFSET +
|
||||
FOLLOWERS_OFFSET +
|
||||
REPO_OFFSET;
|
||||
|
||||
const RANK_S_VALUE = 1;
|
||||
const RANK_DOUBLE_A_VALUE = 25;
|
||||
const RANK_A2_VALUE = 45;
|
||||
const RANK_A3_VALUE = 60;
|
||||
const RANK_B_VALUE = 100;
|
||||
|
||||
const TOTAL_VALUES =
|
||||
RANK_S_VALUE + RANK_A2_VALUE + RANK_A3_VALUE + RANK_B_VALUE;
|
||||
|
||||
// prettier-ignore
|
||||
const score = (
|
||||
@@ -27,26 +57,37 @@ function calculateRank({
|
||||
issues * ISSUES_OFFSET +
|
||||
stargazers * STARS_OFFSET +
|
||||
prs * PRS_OFFSET +
|
||||
followers * FOLLOWERS_OFFSET
|
||||
) / totalRepos;
|
||||
followers * FOLLOWERS_OFFSET +
|
||||
totalRepos * REPO_OFFSET
|
||||
) / 100;
|
||||
|
||||
const normalizedScore = normalcdf(score, TOTAL_VALUES, ALL_OFFSETS) * 100;
|
||||
|
||||
let level = "";
|
||||
|
||||
if (score == FIRST_STEP) {
|
||||
level = "B";
|
||||
} else if (score > FIRST_STEP && score <= SECOND_STEP) {
|
||||
level = "B+";
|
||||
} else if (score > SECOND_STEP && score <= THIRD_STEP) {
|
||||
level = "A";
|
||||
} else if (score > THIRD_STEP && score <= FOURTH_STEP) {
|
||||
level = "A+";
|
||||
} else if (score > FOURTH_STEP && score <= FIFTH_STEP) {
|
||||
level = "A++";
|
||||
} else if (score > FIFTH_STEP) {
|
||||
if (normalizedScore < RANK_S_VALUE) {
|
||||
level = "S+";
|
||||
}
|
||||
if (
|
||||
normalizedScore >= RANK_S_VALUE &&
|
||||
normalizedScore < RANK_DOUBLE_A_VALUE
|
||||
) {
|
||||
level = "S";
|
||||
}
|
||||
if (
|
||||
normalizedScore >= RANK_DOUBLE_A_VALUE &&
|
||||
normalizedScore < RANK_A2_VALUE
|
||||
) {
|
||||
level = "A++";
|
||||
}
|
||||
if (normalizedScore >= RANK_A2_VALUE && normalizedScore < RANK_A3_VALUE) {
|
||||
level = "A+";
|
||||
}
|
||||
if (normalizedScore >= RANK_A3_VALUE && normalizedScore < RANK_B_VALUE) {
|
||||
level = "B+";
|
||||
}
|
||||
|
||||
return { level, score };
|
||||
return { level, score: normalizedScore };
|
||||
}
|
||||
|
||||
module.exports = calculateRank;
|
||||
|
||||
+32
-15
@@ -1,14 +1,16 @@
|
||||
const { request } = require("./utils");
|
||||
const retryer = require("./retryer");
|
||||
|
||||
async function fetchRepo(username, reponame) {
|
||||
if (!username || !reponame) {
|
||||
throw new Error("Invalid username or reponame");
|
||||
}
|
||||
|
||||
const res = await request({
|
||||
query: `
|
||||
const fetcher = (variables, token) => {
|
||||
return request(
|
||||
{
|
||||
query: `
|
||||
fragment RepoInfo on Repository {
|
||||
name
|
||||
nameWithOwner
|
||||
isPrivate
|
||||
isArchived
|
||||
isTemplate
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
@@ -33,11 +35,20 @@ async function fetchRepo(username, reponame) {
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
login: username,
|
||||
repo: reponame,
|
||||
variables,
|
||||
},
|
||||
});
|
||||
{
|
||||
Authorization: `bearer ${token}`,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
async function fetchRepo(username, reponame) {
|
||||
if (!username || !reponame) {
|
||||
throw new Error("Invalid username or reponame");
|
||||
}
|
||||
|
||||
let res = await retryer(fetcher, { login: username, repo: reponame });
|
||||
|
||||
const data = res.data.data;
|
||||
|
||||
@@ -45,15 +56,21 @@ async function fetchRepo(username, reponame) {
|
||||
throw new Error("Not found");
|
||||
}
|
||||
|
||||
if (data.organization === null && data.user) {
|
||||
if (!data.user.repository) {
|
||||
const isUser = data.organization === null && data.user;
|
||||
const isOrg = data.user === null && data.organization;
|
||||
|
||||
if (isUser) {
|
||||
if (!data.user.repository || data.user.repository.isPrivate) {
|
||||
throw new Error("User Repository Not found");
|
||||
}
|
||||
return data.user.repository;
|
||||
}
|
||||
|
||||
if (data.user === null && data.organization) {
|
||||
if (!data.organization.repository) {
|
||||
if (isOrg) {
|
||||
if (
|
||||
!data.organization.repository ||
|
||||
data.organization.repository.isPrivate
|
||||
) {
|
||||
throw new Error("Organization Repository Not found");
|
||||
}
|
||||
return data.organization.repository;
|
||||
|
||||
+38
-19
@@ -1,32 +1,33 @@
|
||||
const { request } = require("./utils");
|
||||
const { request, logger } = require("./utils");
|
||||
const retryer = require("./retryer");
|
||||
const calculateRank = require("./calculateRank");
|
||||
require("dotenv").config();
|
||||
|
||||
async function fetchStats(username) {
|
||||
if (!username) throw Error("Invalid username");
|
||||
|
||||
const res = await request({
|
||||
query: `
|
||||
const fetcher = (variables, token) => {
|
||||
return request(
|
||||
{
|
||||
query: `
|
||||
query userInfo($login: String!) {
|
||||
user(login: $login) {
|
||||
name
|
||||
login
|
||||
repositoriesContributedTo(first: 100, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
|
||||
totalCount
|
||||
}
|
||||
contributionsCollection {
|
||||
totalCommitContributions
|
||||
restrictedContributionsCount
|
||||
}
|
||||
pullRequests(first: 100) {
|
||||
repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
|
||||
totalCount
|
||||
}
|
||||
issues(first: 100) {
|
||||
pullRequests(first: 1) {
|
||||
totalCount
|
||||
}
|
||||
issues(first: 1) {
|
||||
totalCount
|
||||
}
|
||||
followers {
|
||||
totalCount
|
||||
}
|
||||
repositories(first: 100, orderBy: { direction: DESC, field: STARGAZERS }) {
|
||||
repositories(first: 100, ownerAffiliations: OWNER, isFork: false, orderBy: {direction: DESC, field: STARGAZERS}) {
|
||||
totalCount
|
||||
nodes {
|
||||
stargazers {
|
||||
@@ -36,9 +37,17 @@ async function fetchStats(username) {
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { login: username },
|
||||
});
|
||||
`,
|
||||
variables,
|
||||
},
|
||||
{
|
||||
Authorization: `bearer ${token}`,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
async function fetchStats(username, count_private = false) {
|
||||
if (!username) throw Error("Invalid username");
|
||||
|
||||
const stats = {
|
||||
name: "",
|
||||
@@ -47,19 +56,29 @@ async function fetchStats(username) {
|
||||
totalIssues: 0,
|
||||
totalStars: 0,
|
||||
contributedTo: 0,
|
||||
rank: "C",
|
||||
rank: { level: "C", score: 0 },
|
||||
};
|
||||
|
||||
let res = await retryer(fetcher, { login: username });
|
||||
|
||||
if (res.data.errors) {
|
||||
console.log(res.data.errors);
|
||||
throw Error("Could not fetch user");
|
||||
logger.error(res.data.errors);
|
||||
throw Error(res.data.errors[0].message || "Could not fetch user");
|
||||
}
|
||||
|
||||
const user = res.data.data.user;
|
||||
const contributionCount = user.contributionsCollection;
|
||||
|
||||
stats.name = user.name || user.login;
|
||||
stats.totalIssues = user.issues.totalCount;
|
||||
stats.totalCommits = user.contributionsCollection.totalCommitContributions;
|
||||
|
||||
stats.totalCommits = contributionCount.totalCommitContributions;
|
||||
if (count_private) {
|
||||
stats.totalCommits =
|
||||
contributionCount.totalCommitContributions +
|
||||
contributionCount.restrictedContributionsCount;
|
||||
}
|
||||
|
||||
stats.totalPRs = user.pullRequests.totalCount;
|
||||
stats.contributedTo = user.repositoriesContributedTo.totalCount;
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
const { request, logger } = require("./utils");
|
||||
const retryer = require("./retryer");
|
||||
require("dotenv").config();
|
||||
|
||||
const fetcher = (variables, token) => {
|
||||
return request(
|
||||
{
|
||||
query: `
|
||||
query userInfo($login: String!) {
|
||||
user(login: $login) {
|
||||
repositories(isFork: false, first: 100) {
|
||||
nodes {
|
||||
languages(first: 1) {
|
||||
edges {
|
||||
size
|
||||
node {
|
||||
color
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables,
|
||||
},
|
||||
{
|
||||
Authorization: `bearer ${token}`,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
async function fetchTopLanguages(username) {
|
||||
if (!username) throw Error("Invalid username");
|
||||
|
||||
let res = await retryer(fetcher, { login: username });
|
||||
|
||||
if (res.data.errors) {
|
||||
logger.error(res.data.errors);
|
||||
throw Error(res.data.errors[0].message || "Could not fetch user");
|
||||
}
|
||||
|
||||
let repoNodes = res.data.data.user.repositories.nodes;
|
||||
|
||||
// TODO: perf improvement
|
||||
repoNodes = repoNodes
|
||||
.filter((node) => {
|
||||
return node.languages.edges.length > 0;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return b.languages.edges[0].size - a.languages.edges[0].size;
|
||||
})
|
||||
.map((node) => {
|
||||
return node.languages.edges[0];
|
||||
})
|
||||
.reduce((acc, prev) => {
|
||||
let langSize = prev.size;
|
||||
if (acc[prev.node.name] && prev.node.name === acc[prev.node.name].name) {
|
||||
langSize = prev.size + acc[prev.node.name].size;
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[prev.node.name]: {
|
||||
name: prev.node.name,
|
||||
color: prev.node.color,
|
||||
size: langSize,
|
||||
},
|
||||
};
|
||||
}, {});
|
||||
|
||||
const topLangs = Object.keys(repoNodes)
|
||||
.slice(0, 5)
|
||||
.reduce((result, key) => {
|
||||
result[key] = repoNodes[key];
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
return topLangs;
|
||||
}
|
||||
|
||||
module.exports = fetchTopLanguages;
|
||||
+1
-4
@@ -52,7 +52,7 @@ const getStyles = ({
|
||||
animation: fadeIn 0.8s ease-in-out forwards;
|
||||
}
|
||||
.stat {
|
||||
font: 600 14px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor};
|
||||
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
|
||||
}
|
||||
.stagger {
|
||||
opacity: 0;
|
||||
@@ -64,9 +64,6 @@ const getStyles = ({
|
||||
}
|
||||
|
||||
.bold { font-weight: 700 }
|
||||
.star-icon {
|
||||
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
|
||||
}
|
||||
.icon {
|
||||
fill: ${iconColor};
|
||||
display: ${!!show_icons ? "block" : "none"};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
const icons = {
|
||||
star: `<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>`,
|
||||
commits: `<path fill-rule="evenodd" d="M1.643 3.143L.427 1.927A.25.25 0 000 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 00.177-.427L2.715 4.215a6.5 6.5 0 11-1.18 4.458.75.75 0 10-1.493.154 8.001 8.001 0 101.6-5.684zM7.75 4a.75.75 0 01.75.75v2.992l2.028.812a.75.75 0 01-.557 1.392l-2.5-1A.75.75 0 017 8.25v-3.5A.75.75 0 017.75 4z"/>`,
|
||||
prs: `<path fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"/>`,
|
||||
issues: `<path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"/>`,
|
||||
icon: `<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>`,
|
||||
contribs: `<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>`,
|
||||
fork: `<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path>`,
|
||||
};
|
||||
|
||||
module.exports = icons;
|
||||
+109
-35
@@ -1,64 +1,138 @@
|
||||
const { kFormatter, encodeHTML, isValidHexColor } = require("../src/utils");
|
||||
const {
|
||||
kFormatter,
|
||||
encodeHTML,
|
||||
getCardColors,
|
||||
FlexLayout,
|
||||
} = require("../src/utils");
|
||||
const icons = require("./icons");
|
||||
const toEmoji = require("emoji-name-map");
|
||||
|
||||
const renderRepoCard = (repo, options = {}) => {
|
||||
const { name, description, primaryLanguage, stargazers, forkCount } = repo;
|
||||
const { title_color, icon_color, text_color, bg_color } = options;
|
||||
const {
|
||||
name,
|
||||
nameWithOwner,
|
||||
description,
|
||||
primaryLanguage,
|
||||
stargazers,
|
||||
isArchived,
|
||||
isTemplate,
|
||||
forkCount,
|
||||
} = repo;
|
||||
const {
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
show_owner,
|
||||
theme = "default_repocard",
|
||||
} = options;
|
||||
|
||||
const header = show_owner ? nameWithOwner : name;
|
||||
const langName = (primaryLanguage && primaryLanguage.name) || "Unspecified";
|
||||
const langColor = (primaryLanguage && primaryLanguage.color) || "#333";
|
||||
|
||||
const height = 120;
|
||||
const shiftText = primaryLanguage.name.length > 15 ? 0 : 30;
|
||||
const shiftText = langName.length > 15 ? 0 : 30;
|
||||
|
||||
let desc = description || "No description provided";
|
||||
|
||||
// parse emojis to unicode
|
||||
desc = desc.replace(/:\w+:/gm, (emoji) => {
|
||||
return toEmoji.get(emoji) || "";
|
||||
});
|
||||
|
||||
if (desc.length > 55) {
|
||||
desc = `${description.slice(0, 55)}..`;
|
||||
desc = `${desc.slice(0, 55)}..`;
|
||||
}
|
||||
|
||||
const titleColor =
|
||||
(isValidHexColor(title_color) && `#${title_color}`) || "#2f80ed";
|
||||
const iconColor =
|
||||
(isValidHexColor(icon_color) && `#${icon_color}`) || "#586069";
|
||||
const textColor = (isValidHexColor(text_color) && `#${text_color}`) || "#333";
|
||||
const bgColor = (isValidHexColor(bg_color) && `#${bg_color}`) || "#FFFEFE";
|
||||
// returns theme based colors with proper overrides and defaults
|
||||
const { titleColor, textColor, iconColor, bgColor } = getCardColors({
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
});
|
||||
|
||||
const totalStars = kFormatter(stargazers.totalCount);
|
||||
const totalForks = kFormatter(forkCount);
|
||||
|
||||
const getBadgeSVG = (label) => `
|
||||
<g data-testid="badge" class="badge" transform="translate(320, 38)">
|
||||
<rect stroke="${textColor}" stroke-width="1" width="70" height="20" x="-12" y="-14" ry="10" rx="10"></rect>
|
||||
<text
|
||||
x="23" y="-5"
|
||||
alignment-baseline="central"
|
||||
dominant-baseline="central"
|
||||
text-anchor="middle"
|
||||
fill="${textColor}"
|
||||
>
|
||||
${label}
|
||||
</text>
|
||||
</g>
|
||||
`;
|
||||
|
||||
const svgLanguage = primaryLanguage
|
||||
? `
|
||||
<g data-testid="primary-lang" transform="translate(30, 100)">
|
||||
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="${langColor}" />
|
||||
<text data-testid="lang-name" class="gray" x="15">${langName}</text>
|
||||
</g>
|
||||
`
|
||||
: "";
|
||||
|
||||
const svgStars =
|
||||
stargazers.totalCount > 0 &&
|
||||
`
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
${icons.star}
|
||||
</svg>
|
||||
<text data-testid="stargazers" class="gray" x="25">${totalStars}</text>
|
||||
`;
|
||||
|
||||
const svgForks =
|
||||
forkCount > 0 &&
|
||||
`
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
${icons.fork}
|
||||
</svg>
|
||||
<text data-testid="forkcount" class="gray" x="25">${totalForks}</text>
|
||||
`;
|
||||
|
||||
return `
|
||||
<svg width="400" height="${height}" viewBox="0 0 400 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg version="1.1" width="400" height="${height}" viewBox="0 0 400 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.header { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor} }
|
||||
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
|
||||
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
|
||||
.icon { fill: ${iconColor} }
|
||||
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
|
||||
.badge rect { opacity: 0.2 }
|
||||
</style>
|
||||
<rect data-testid="card-border" x="0.5" y="0.5" width="399" height="99%" rx="4.5" fill="${bgColor}" stroke="#E4E2E2"/>
|
||||
|
||||
<rect data-testid="card-bg" x="0.5" y="0.5" width="399" height="99%" rx="4.5" fill="${bgColor}" stroke="#E4E2E2"/>
|
||||
<svg class="icon" x="25" y="25" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"></path>
|
||||
${icons.contribs}
|
||||
</svg>
|
||||
|
||||
<text x="50" y="38" class="header">${name}</text>
|
||||
<text x="50" y="38" class="header">${header}</text>
|
||||
|
||||
${
|
||||
isTemplate
|
||||
? getBadgeSVG("Template")
|
||||
: isArchived
|
||||
? getBadgeSVG("Archived")
|
||||
: ""
|
||||
}
|
||||
|
||||
<text class="description" x="25" y="70">${encodeHTML(desc)}</text>
|
||||
|
||||
<g transform="translate(30, 100)">
|
||||
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="${
|
||||
primaryLanguage.color
|
||||
}" />
|
||||
<text data-testid="lang" class="gray" x="15">${
|
||||
primaryLanguage.name
|
||||
}</text>
|
||||
${svgLanguage}
|
||||
|
||||
<g transform="translate(${primaryLanguage ? 155 - shiftText : 25}, 100)">
|
||||
${FlexLayout({ items: [svgStars, svgForks], gap: 65 }).join("")}
|
||||
</g>
|
||||
|
||||
<g transform="translate(${155 - shiftText}, 100)">
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path>
|
||||
</svg>
|
||||
<text data-testid="stargazers" class="gray" x="25">${totalStars}</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(${220 - shiftText}, 100)">
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path>
|
||||
</svg>
|
||||
<text data-testid="forkcount" class="gray" x="25">${totalForks}</text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
|
||||
+93
-59
@@ -1,21 +1,30 @@
|
||||
const { kFormatter, isValidHexColor } = require("../src/utils");
|
||||
const {
|
||||
kFormatter,
|
||||
getCardColors,
|
||||
FlexLayout,
|
||||
encodeHTML,
|
||||
} = require("../src/utils");
|
||||
const getStyles = require("./getStyles");
|
||||
const icons = require("./icons");
|
||||
|
||||
const createTextNode = ({ icon, label, value, id, index, lineHeight }) => {
|
||||
const classname = icon === "★" && "star-icon";
|
||||
const createTextNode = ({ icon, label, value, id, index, showIcons }) => {
|
||||
const kValue = kFormatter(value);
|
||||
const staggerDelay = (index + 3) * 150;
|
||||
// manually calculating lineHeight based on index instead of using <tspan dy="" />
|
||||
// to fix firefox layout bug
|
||||
const lheight = lineHeight * (index + 1);
|
||||
|
||||
const labelOffset = showIcons ? `x="25"` : "";
|
||||
const iconSvg = showIcons
|
||||
? `
|
||||
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
${icon}
|
||||
</svg>
|
||||
`
|
||||
: "";
|
||||
return `
|
||||
<text class="stagger" style="animation-delay: ${staggerDelay}ms" x="25" y="${lheight}">
|
||||
<tspan dx="0" data-testid="icon" class="icon ${classname}">${icon}</tspan>
|
||||
<tspan dx="0" class="stat bold">
|
||||
${label}:
|
||||
</tspan>
|
||||
<tspan x="160" data-testid="${id}" class="stat">${kValue}</tspan>
|
||||
</text>
|
||||
<g class="stagger" style="animation-delay: ${staggerDelay}ms" transform="translate(25, 0)">
|
||||
${iconSvg}
|
||||
<text class="stat bold" ${labelOffset} y="12.5">${label}:</text>
|
||||
<text class="stat" x="135" y="12.5" data-testid="${id}">${kValue}</text>
|
||||
</g>
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -32,6 +41,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
const {
|
||||
hide = [],
|
||||
show_icons = false,
|
||||
hide_title = false,
|
||||
hide_border = false,
|
||||
hide_rank = false,
|
||||
line_height = 25,
|
||||
@@ -39,67 +49,95 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme = "default",
|
||||
} = options;
|
||||
|
||||
const lheight = parseInt(line_height);
|
||||
|
||||
const titleColor =
|
||||
(isValidHexColor(title_color) && `#${title_color}`) || "#2f80ed";
|
||||
const iconColor =
|
||||
(isValidHexColor(icon_color) && `#${icon_color}`) || "#4c71f2";
|
||||
const textColor = (isValidHexColor(text_color) && `#${text_color}`) || "#333";
|
||||
const bgColor = (isValidHexColor(bg_color) && `#${bg_color}`) || "#FFFEFE";
|
||||
// returns theme based colors with proper overrides and defaults
|
||||
const { titleColor, textColor, iconColor, bgColor } = getCardColors({
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
});
|
||||
|
||||
// Meta data for creating text nodes with createTextNode function
|
||||
const STATS = {
|
||||
stars: {
|
||||
icon: "★",
|
||||
icon: icons.star,
|
||||
label: "Total Stars",
|
||||
value: totalStars,
|
||||
id: "stars",
|
||||
},
|
||||
commits: {
|
||||
icon: "🕗",
|
||||
icon: icons.commits,
|
||||
label: "Total Commits",
|
||||
value: totalCommits,
|
||||
id: "commits",
|
||||
},
|
||||
prs: {
|
||||
icon: "🔀",
|
||||
icon: icons.prs,
|
||||
label: "Total PRs",
|
||||
value: totalPRs,
|
||||
id: "prs",
|
||||
},
|
||||
issues: {
|
||||
icon: "ⓘ",
|
||||
icon: icons.issues,
|
||||
label: "Total Issues",
|
||||
value: totalIssues,
|
||||
id: "issues",
|
||||
},
|
||||
contribs: {
|
||||
icon: "📕",
|
||||
icon: icons.contribs,
|
||||
label: "Contributed to",
|
||||
value: contributedTo,
|
||||
id: "contribs",
|
||||
},
|
||||
};
|
||||
|
||||
// filter out hidden stats defined by user & create the text nodes
|
||||
const statItems = Object.keys(STATS)
|
||||
.filter((key) => !hide.includes(key))
|
||||
.map((key, index) =>
|
||||
// create the text nodes, and pass index so that we can calculate the line spacing
|
||||
createTextNode({ ...STATS[key], index, lineHeight: lheight })
|
||||
createTextNode({
|
||||
...STATS[key],
|
||||
index,
|
||||
showIcons: show_icons,
|
||||
})
|
||||
);
|
||||
|
||||
// Calculate the card height depending on how many items there are
|
||||
// but if rank circle is visible clamp the minimum height to `150`
|
||||
const height = Math.max(
|
||||
let height = Math.max(
|
||||
45 + (statItems.length + 1) * lheight,
|
||||
hide_rank ? 0 : 150
|
||||
);
|
||||
|
||||
// the better user's score the the rank will be closer to zero so
|
||||
// subtracting 100 to get the progress in 100%
|
||||
const progress = 100 - rank.score;
|
||||
|
||||
const styles = getStyles({
|
||||
titleColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
show_icons,
|
||||
progress,
|
||||
});
|
||||
|
||||
// Conditionally rendered elements
|
||||
|
||||
const apostrophe = ["x", "s"].includes(name.slice(-1)) ? "" : "s";
|
||||
const title = hide_title
|
||||
? ""
|
||||
: `<text x="25" y="35" class="header">${encodeHTML(name)}'${apostrophe} GitHub Stats</text>`;
|
||||
|
||||
const border = `
|
||||
<rect
|
||||
data-testid="card-border"
|
||||
data-testid="card-bg"
|
||||
x="0.5"
|
||||
y="0.5"
|
||||
width="494"
|
||||
@@ -107,6 +145,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
rx="4.5"
|
||||
fill="${bgColor}"
|
||||
stroke="#E4E2E2"
|
||||
stroke-opacity="${hide_border ? 0 : 1}"
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -117,34 +156,22 @@ 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" />
|
||||
<text
|
||||
x="0"
|
||||
y="0"
|
||||
alignment-baseline="central"
|
||||
dominant-baseline="central"
|
||||
text-anchor="middle"
|
||||
class="rank-text"
|
||||
>
|
||||
${rank.level}
|
||||
</text>
|
||||
<g class="rank-text">
|
||||
<text
|
||||
x="${rank.level.length === 1 ? "-4" : "0"}"
|
||||
y="0"
|
||||
alignment-baseline="central"
|
||||
dominant-baseline="central"
|
||||
text-anchor="middle"
|
||||
>
|
||||
${rank.level}
|
||||
</text>
|
||||
</g>
|
||||
</g>`;
|
||||
|
||||
// re-adjust circle progressbar's value until the ranking algo is improved
|
||||
let progress = rank.score;
|
||||
if (rank.score > 86) {
|
||||
progress = (40 + rank.score) * 0.6;
|
||||
if (hide_title) {
|
||||
height -= 30;
|
||||
}
|
||||
if (rank.score < 40) {
|
||||
progress = 40 + rank.score;
|
||||
}
|
||||
|
||||
const styles = getStyles({
|
||||
titleColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
show_icons,
|
||||
progress,
|
||||
});
|
||||
|
||||
return `
|
||||
<svg width="495" height="${height}" viewBox="0 0 495 ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -152,15 +179,22 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
${styles}
|
||||
</style>
|
||||
|
||||
${hide_border ? "" : border}
|
||||
${border}
|
||||
${title}
|
||||
|
||||
${rankCircle}
|
||||
|
||||
<text x="25" y="35" class="header">${name}'s GitHub Stats</text>
|
||||
<g data-testid="card-body-content" transform="translate(0, ${
|
||||
hide_title ? -30 : 0
|
||||
})">
|
||||
${rankCircle}
|
||||
|
||||
<svg x="0" y="45">
|
||||
${statItems.toString().replace(/\,/gm, "")}
|
||||
</svg>
|
||||
<svg x="0" y="55">
|
||||
${FlexLayout({
|
||||
items: statItems,
|
||||
gap: lheight,
|
||||
direction: "column",
|
||||
}).join("")}
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
const { getCardColors, FlexLayout, clampValue } = require("../src/utils");
|
||||
|
||||
const createProgressNode = ({ width, color, name, progress }) => {
|
||||
const paddingRight = 95;
|
||||
const progressTextX = width - paddingRight + 10;
|
||||
const progressWidth = width - paddingRight;
|
||||
const progressPercentage = clampValue(progress, 2, 100);
|
||||
|
||||
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>
|
||||
<svg width="${progressWidth}">
|
||||
<rect rx="5" ry="5" x="0" y="25" width="${progressWidth}" height="8" fill="#ddd"></rect>
|
||||
<rect
|
||||
height="8"
|
||||
fill="${color}"
|
||||
rx="5" ry="5" x="0" y="25"
|
||||
data-testid="lang-progress"
|
||||
width="${progressPercentage}%"
|
||||
>
|
||||
</rect>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
|
||||
const lowercaseTrim = (name) => name.toLowerCase().trim();
|
||||
|
||||
const renderTopLanguages = (topLangs, options = {}) => {
|
||||
const {
|
||||
hide_title,
|
||||
card_width,
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
hide,
|
||||
theme,
|
||||
} = options;
|
||||
|
||||
let langs = Object.values(topLangs);
|
||||
let langsToHide = {};
|
||||
|
||||
// populate langsToHide map for quick lookup
|
||||
// while filtering out
|
||||
if (hide) {
|
||||
hide.forEach((langName) => {
|
||||
langsToHide[lowercaseTrim(langName)] = true;
|
||||
});
|
||||
}
|
||||
|
||||
// filter out langauges to be hidden
|
||||
langs = langs
|
||||
.sort((a, b) => b.size - a.size)
|
||||
.filter((lang) => {
|
||||
return !langsToHide[lowercaseTrim(lang.name)];
|
||||
});
|
||||
|
||||
const totalSize = langs.reduce((acc, curr) => {
|
||||
return acc + curr.size;
|
||||
}, 0);
|
||||
|
||||
// returns theme based colors with proper overrides and defaults
|
||||
const { titleColor, textColor, bgColor } = getCardColors({
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
});
|
||||
|
||||
const width = isNaN(card_width) ? 300 : card_width;
|
||||
let height = 45 + (langs.length + 1) * 40;
|
||||
|
||||
if (hide_title) {
|
||||
height -= 30;
|
||||
}
|
||||
return `
|
||||
<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.header { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor} }
|
||||
.lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
|
||||
</style>
|
||||
<rect data-testid="card-bg" x="0.5" y="0.5" width="99.7%" height="99%" rx="4.5" fill="${bgColor}" stroke="#E4E2E2"/>
|
||||
|
||||
|
||||
${
|
||||
hide_title
|
||||
? ""
|
||||
: `<text data-testid="header" x="25" y="35" class="header">Top Languages</text>`
|
||||
}
|
||||
|
||||
<svg data-testid="lang-items" x="25" y="${hide_title ? 25 : 55}">
|
||||
${FlexLayout({
|
||||
items: langs.map((lang) => {
|
||||
return createProgressNode({
|
||||
width: width,
|
||||
name: lang.name,
|
||||
color: lang.color || "#858585",
|
||||
progress: ((lang.size / totalSize) * 100).toFixed(2),
|
||||
});
|
||||
}),
|
||||
gap: 40,
|
||||
direction: "column",
|
||||
}).join("")}
|
||||
</svg>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
|
||||
module.exports = renderTopLanguages;
|
||||
@@ -0,0 +1,45 @@
|
||||
const { logger } = require("./utils");
|
||||
|
||||
const retryer = async (fetcher, variables, retries = 0) => {
|
||||
if (retries > 7) {
|
||||
throw new Error("Maximum retries exceeded");
|
||||
}
|
||||
try {
|
||||
logger.log(`Trying PAT_${retries + 1}`);
|
||||
|
||||
// try to fetch with the first token since RETRIES is 0 index i'm adding +1
|
||||
let response = await fetcher(
|
||||
variables,
|
||||
process.env[`PAT_${retries + 1}`],
|
||||
retries
|
||||
);
|
||||
|
||||
// prettier-ignore
|
||||
const isRateExceeded = response.data.errors && response.data.errors[0].type === "RATE_LIMITED";
|
||||
|
||||
// if rate limit is hit increase the RETRIES and recursively call the retryer
|
||||
// with username, and current RETRIES
|
||||
if (isRateExceeded) {
|
||||
logger.log(`PAT_${retries + 1} Failed`);
|
||||
retries++;
|
||||
// directly return from the function
|
||||
return retryer(fetcher, variables, retries);
|
||||
}
|
||||
|
||||
// finally return the response
|
||||
return response;
|
||||
} catch (err) {
|
||||
// prettier-ignore
|
||||
// also checking for bad credentials if any tokens gets invalidated
|
||||
const isBadCredential = err.response.data && err.response.data.message === "Bad credentials";
|
||||
|
||||
if (isBadCredential) {
|
||||
logger.log(`PAT_${retries + 1} Failed`);
|
||||
retries++;
|
||||
// directly return from the function
|
||||
return retryer(fetcher, variables, retries);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = retryer;
|
||||
+123
-19
@@ -1,24 +1,31 @@
|
||||
const axios = require("axios");
|
||||
const themes = require("../themes");
|
||||
|
||||
const renderError = (message) => {
|
||||
const renderError = (message, secondaryMessage = "") => {
|
||||
return `
|
||||
<svg width="495" height="100" viewBox="0 0 495 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="495" height="120" viewBox="0 0 495 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2F80ED }
|
||||
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #252525 }
|
||||
.gray { fill: #858585 }
|
||||
</style>
|
||||
<rect x="0.5" y="0.5" width="494" height="99%" rx="4.5" fill="#FFFEFE" stroke="#E4E2E2"/>
|
||||
<text x="25" y="45" class="text">Something went wrong! file an issue at https://git.io/JJmN9</text>
|
||||
<text id="message" x="25" y="65" class="text small">${message}</text>
|
||||
<text data-testid="message" x="25" y="55" class="text small">
|
||||
<tspan x="25" dy="18">${message}</tspan>
|
||||
<tspan x="25" dy="18" class="gray">${secondaryMessage}</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
|
||||
// https://stackoverflow.com/a/48073476/10629172
|
||||
function encodeHTML(str) {
|
||||
return str.replace(/[\u00A0-\u9999<>&](?!#)/gim, function (i) {
|
||||
return "&#" + i.charCodeAt(0) + ";";
|
||||
});
|
||||
return str
|
||||
.replace(/[\u00A0-\u9999<>&](?!#)/gim, (i) => {
|
||||
return "&#" + i.charCodeAt(0) + ";";
|
||||
})
|
||||
.replace(/\u0008/gim, "");
|
||||
}
|
||||
|
||||
function kFormatter(num) {
|
||||
@@ -33,19 +40,116 @@ function isValidHexColor(hexColor) {
|
||||
).test(hexColor);
|
||||
}
|
||||
|
||||
function request(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios({
|
||||
url: "https://api.github.com/graphql",
|
||||
method: "post",
|
||||
headers: {
|
||||
Authorization: `bearer ${process.env.GITHUB_TOKEN}`,
|
||||
},
|
||||
data,
|
||||
})
|
||||
.then((response) => resolve(response))
|
||||
.catch((error) => reject(error));
|
||||
function parseBoolean(value) {
|
||||
if (value === "true") {
|
||||
return true;
|
||||
} else if (value === "false") {
|
||||
return false;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function parseArray(str) {
|
||||
if (!str) return [];
|
||||
return str.split(",");
|
||||
}
|
||||
|
||||
function clampValue(number, min, max) {
|
||||
return Math.max(min, Math.min(number, max));
|
||||
}
|
||||
|
||||
function fallbackColor(color, fallbackColor) {
|
||||
return (isValidHexColor(color) && `#${color}`) || fallbackColor;
|
||||
}
|
||||
|
||||
function request(data, headers) {
|
||||
return axios({
|
||||
url: "https://api.github.com/graphql",
|
||||
method: "post",
|
||||
headers,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { renderError, kFormatter, encodeHTML, isValidHexColor, request };
|
||||
/**
|
||||
*
|
||||
* @param {String[]} items
|
||||
* @param {Number} gap
|
||||
* @param {string} direction
|
||||
*
|
||||
* @description
|
||||
* Auto layout utility, allows us to layout things
|
||||
* vertically or horizontally with proper gaping
|
||||
*/
|
||||
function FlexLayout({ items, gap, direction }) {
|
||||
// filter() for filtering out empty strings
|
||||
return items.filter(Boolean).map((item, i) => {
|
||||
let transform = `translate(${gap * i}, 0)`;
|
||||
if (direction === "column") {
|
||||
transform = `translate(0, ${gap * i})`;
|
||||
}
|
||||
return `<g transform="${transform}">${item}</g>`;
|
||||
});
|
||||
}
|
||||
|
||||
// returns theme based colors with proper overrides and defaults
|
||||
function getCardColors({
|
||||
title_color,
|
||||
text_color,
|
||||
icon_color,
|
||||
bg_color,
|
||||
theme,
|
||||
fallbackTheme = "default",
|
||||
}) {
|
||||
const defaultTheme = themes[fallbackTheme];
|
||||
const selectedTheme = themes[theme] || defaultTheme;
|
||||
|
||||
// get the color provided by the user else the theme color
|
||||
// finally if both colors are invalid fallback to default theme
|
||||
const titleColor = fallbackColor(
|
||||
title_color || selectedTheme.title_color,
|
||||
"#" + defaultTheme.title_color
|
||||
);
|
||||
const iconColor = fallbackColor(
|
||||
icon_color || selectedTheme.icon_color,
|
||||
"#" + defaultTheme.icon_color
|
||||
);
|
||||
const textColor = fallbackColor(
|
||||
text_color || selectedTheme.text_color,
|
||||
"#" + defaultTheme.text_color
|
||||
);
|
||||
const bgColor = fallbackColor(
|
||||
bg_color || selectedTheme.bg_color,
|
||||
"#" + defaultTheme.bg_color
|
||||
);
|
||||
|
||||
return { titleColor, iconColor, textColor, bgColor };
|
||||
}
|
||||
|
||||
const fn = () => {};
|
||||
// return console instance based on the environment
|
||||
const logger =
|
||||
process.env.NODE_ENV !== "test" ? console : { log: fn, error: fn };
|
||||
|
||||
const CONSTANTS = {
|
||||
THIRTY_MINUTES: 1800,
|
||||
TWO_HOURS: 7200,
|
||||
ONE_DAY: 86400,
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
renderError,
|
||||
kFormatter,
|
||||
encodeHTML,
|
||||
isValidHexColor,
|
||||
request,
|
||||
parseArray,
|
||||
parseBoolean,
|
||||
fallbackColor,
|
||||
FlexLayout,
|
||||
getCardColors,
|
||||
clampValue,
|
||||
logger,
|
||||
CONSTANTS,
|
||||
};
|
||||
|
||||
+121
-32
@@ -3,7 +3,7 @@ const axios = require("axios");
|
||||
const MockAdapter = require("axios-mock-adapter");
|
||||
const api = require("../api/index");
|
||||
const renderStatsCard = require("../src/renderStatsCard");
|
||||
const { renderError } = require("../src/utils");
|
||||
const { renderError, CONSTANTS } = require("../src/utils");
|
||||
const calculateRank = require("../src/calculateRank");
|
||||
|
||||
const stats = {
|
||||
@@ -30,7 +30,10 @@ const data = {
|
||||
user: {
|
||||
name: stats.name,
|
||||
repositoriesContributedTo: { totalCount: stats.contributedTo },
|
||||
contributionsCollection: { totalCommitContributions: stats.totalCommits },
|
||||
contributionsCollection: {
|
||||
totalCommitContributions: stats.totalCommits,
|
||||
restrictedContributionsCount: 100,
|
||||
},
|
||||
pullRequests: { totalCount: stats.totalPRs },
|
||||
issues: { totalCount: stats.totalIssues },
|
||||
followers: { totalCount: 0 },
|
||||
@@ -55,22 +58,29 @@ const error = {
|
||||
|
||||
const mock = new MockAdapter(axios);
|
||||
|
||||
const faker = (query, data) => {
|
||||
const req = {
|
||||
query: {
|
||||
username: "anuraghazra",
|
||||
...query,
|
||||
},
|
||||
};
|
||||
const res = {
|
||||
setHeader: jest.fn(),
|
||||
send: jest.fn(),
|
||||
};
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, data);
|
||||
|
||||
return { req, res };
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
mock.reset();
|
||||
});
|
||||
|
||||
describe("Test /api/", () => {
|
||||
it("should test the request", async () => {
|
||||
const req = {
|
||||
query: {
|
||||
username: "anuraghazra",
|
||||
},
|
||||
};
|
||||
const res = {
|
||||
setHeader: jest.fn(),
|
||||
send: jest.fn(),
|
||||
};
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, data);
|
||||
const { req, res } = faker({}, data);
|
||||
|
||||
await api(req, res);
|
||||
|
||||
@@ -79,28 +89,24 @@ describe("Test /api/", () => {
|
||||
});
|
||||
|
||||
it("should render error card on error", async () => {
|
||||
const req = {
|
||||
query: {
|
||||
username: "anuraghazra",
|
||||
},
|
||||
};
|
||||
const res = {
|
||||
setHeader: jest.fn(),
|
||||
send: jest.fn(),
|
||||
};
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, error);
|
||||
const { req, res } = faker({}, error);
|
||||
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
|
||||
expect(res.send).toBeCalledWith(renderError(error.errors[0].message));
|
||||
expect(res.send).toBeCalledWith(
|
||||
renderError(
|
||||
error.errors[0].message,
|
||||
"Make sure the provided username is not an organization"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it("should get the query options", async () => {
|
||||
const req = {
|
||||
query: {
|
||||
const { req, res } = faker(
|
||||
{
|
||||
username: "anuraghazra",
|
||||
hide: `["issues","prs","contribs"]`,
|
||||
hide: "issues,prs,contribs",
|
||||
show_icons: true,
|
||||
hide_border: true,
|
||||
line_height: 100,
|
||||
@@ -109,12 +115,8 @@ describe("Test /api/", () => {
|
||||
text_color: "fff",
|
||||
bg_color: "fff",
|
||||
},
|
||||
};
|
||||
const res = {
|
||||
setHeader: jest.fn(),
|
||||
send: jest.fn(),
|
||||
};
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, data);
|
||||
data
|
||||
);
|
||||
|
||||
await api(req, res);
|
||||
|
||||
@@ -132,4 +134,91 @@ describe("Test /api/", () => {
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should have proper cache", async () => {
|
||||
const { req, res } = faker({}, data);
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, data);
|
||||
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader.mock.calls).toEqual([
|
||||
["Content-Type", "image/svg+xml"],
|
||||
["Cache-Control", `public, max-age=${CONSTANTS.THIRTY_MINUTES}`],
|
||||
]);
|
||||
});
|
||||
|
||||
it("should set proper cache", async () => {
|
||||
const { req, res } = faker({ cache_seconds: 2000 }, data);
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader.mock.calls).toEqual([
|
||||
["Content-Type", "image/svg+xml"],
|
||||
["Cache-Control", `public, max-age=${2000}`],
|
||||
]);
|
||||
});
|
||||
|
||||
it("should set proper cache with clamped values", async () => {
|
||||
{
|
||||
let { req, res } = faker({ cache_seconds: 200000 }, data);
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader.mock.calls).toEqual([
|
||||
["Content-Type", "image/svg+xml"],
|
||||
["Cache-Control", `public, max-age=${CONSTANTS.ONE_DAY}`],
|
||||
]);
|
||||
}
|
||||
|
||||
// note i'm using block scoped vars
|
||||
{
|
||||
let { req, res } = faker({ cache_seconds: 0 }, data);
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader.mock.calls).toEqual([
|
||||
["Content-Type", "image/svg+xml"],
|
||||
["Cache-Control", `public, max-age=${CONSTANTS.THIRTY_MINUTES}`],
|
||||
]);
|
||||
}
|
||||
|
||||
{
|
||||
let { req, res } = faker({ cache_seconds: -10000 }, data);
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader.mock.calls).toEqual([
|
||||
["Content-Type", "image/svg+xml"],
|
||||
["Cache-Control", `public, max-age=${CONSTANTS.THIRTY_MINUTES}`],
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it("should add private contributions", async () => {
|
||||
const { req, res } = faker(
|
||||
{
|
||||
username: "anuraghazra",
|
||||
count_private: true,
|
||||
},
|
||||
data
|
||||
);
|
||||
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
|
||||
expect(res.send).toBeCalledWith(
|
||||
renderStatsCard(
|
||||
{
|
||||
...stats,
|
||||
totalCommits: stats.totalCommits + 100,
|
||||
rank: calculateRank({
|
||||
totalCommits: stats.totalCommits + 100,
|
||||
totalRepos: 1,
|
||||
followers: 0,
|
||||
contributions: stats.contributedTo,
|
||||
stargazers: stats.totalStars,
|
||||
prs: stats.totalPRs,
|
||||
issues: stats.totalIssues,
|
||||
}),
|
||||
},
|
||||
{}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,6 @@ describe("Test calculateRank", () => {
|
||||
prs: 300,
|
||||
issues: 200,
|
||||
})
|
||||
).toStrictEqual({ level: "S+", score: 192.13 });
|
||||
).toStrictEqual({ level: "A+", score: 49.16605417270399 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,4 +80,17 @@ describe("Test fetchRepo", () => {
|
||||
"Not found"
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error if repository is private", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, {
|
||||
data: {
|
||||
user: { repository: { ...data_repo, isPrivate: true } },
|
||||
organization: null,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow(
|
||||
"User Repository Not found"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ const data = {
|
||||
user: {
|
||||
name: "Anurag Hazra",
|
||||
repositoriesContributedTo: { totalCount: 61 },
|
||||
contributionsCollection: { totalCommitContributions: 100 },
|
||||
contributionsCollection: { totalCommitContributions: 100, restrictedContributionsCount: 50 },
|
||||
pullRequests: { totalCount: 300 },
|
||||
issues: { totalCount: 200 },
|
||||
followers: { totalCount: 100 },
|
||||
@@ -74,7 +74,32 @@ describe("Test fetchStats", () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, error);
|
||||
|
||||
await expect(fetchStats("anuraghazra")).rejects.toThrow(
|
||||
"Could not fetch user"
|
||||
"Could not resolve to a User with the login of 'noname'."
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch and add private contributions", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, data);
|
||||
|
||||
let stats = await fetchStats("anuraghazra", true);
|
||||
const rank = calculateRank({
|
||||
totalCommits: 150,
|
||||
totalRepos: 5,
|
||||
followers: 100,
|
||||
contributions: 61,
|
||||
stargazers: 400,
|
||||
prs: 300,
|
||||
issues: 200,
|
||||
});
|
||||
|
||||
expect(stats).toStrictEqual({
|
||||
contributedTo: 61,
|
||||
name: "Anurag Hazra",
|
||||
totalCommits: 150,
|
||||
totalIssues: 200,
|
||||
totalPRs: 300,
|
||||
totalStars: 400,
|
||||
rank,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
require("@testing-library/jest-dom");
|
||||
const axios = require("axios");
|
||||
const MockAdapter = require("axios-mock-adapter");
|
||||
const fetchTopLanguages = require("../src/fetchTopLanguages");
|
||||
|
||||
const mock = new MockAdapter(axios);
|
||||
|
||||
afterEach(() => {
|
||||
mock.reset();
|
||||
});
|
||||
|
||||
const data_langs = {
|
||||
data: {
|
||||
user: {
|
||||
repositories: {
|
||||
nodes: [
|
||||
{
|
||||
languages: {
|
||||
edges: [{ size: 100, node: { color: "#0f0", name: "HTML" } }],
|
||||
},
|
||||
},
|
||||
{
|
||||
languages: {
|
||||
edges: [{ size: 100, node: { color: "#0f0", name: "HTML" } }],
|
||||
},
|
||||
},
|
||||
{
|
||||
languages: {
|
||||
edges: [
|
||||
{ size: 100, node: { color: "#0ff", name: "javascript" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
languages: {
|
||||
edges: [
|
||||
{ size: 100, node: { color: "#0ff", name: "javascript" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const error = {
|
||||
errors: [
|
||||
{
|
||||
type: "NOT_FOUND",
|
||||
path: ["user"],
|
||||
locations: [],
|
||||
message: "Could not resolve to a User with the login of 'noname'.",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe("FetchTopLanguages", () => {
|
||||
it("should fetch correct language data", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
|
||||
|
||||
let repo = await fetchTopLanguages("anuraghazra");
|
||||
expect(repo).toStrictEqual({
|
||||
HTML: {
|
||||
color: "#0f0",
|
||||
name: "HTML",
|
||||
size: 200,
|
||||
},
|
||||
javascript: {
|
||||
color: "#0ff",
|
||||
name: "javascript",
|
||||
size: 200,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw error", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, error);
|
||||
|
||||
await expect(fetchTopLanguages("anuraghazra")).rejects.toThrow(
|
||||
"Could not resolve to a User with the login of 'noname'."
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ const { renderError } = require("../src/utils");
|
||||
|
||||
const data_repo = {
|
||||
repository: {
|
||||
username: "anuraghazra",
|
||||
name: "convoychat",
|
||||
stargazers: { totalCount: 38000 },
|
||||
description: "Help us take over the world! React + TS + GraphQL Chat App",
|
||||
@@ -16,6 +17,7 @@ const data_repo = {
|
||||
name: "TypeScript",
|
||||
},
|
||||
forkCount: 100,
|
||||
isTemplate: false
|
||||
},
|
||||
};
|
||||
|
||||
@@ -61,6 +63,7 @@ describe("Test /api/pin", () => {
|
||||
icon_color: "fff",
|
||||
text_color: "fff",
|
||||
bg_color: "fff",
|
||||
full_name: "1",
|
||||
},
|
||||
};
|
||||
const res = {
|
||||
|
||||
+177
-10
@@ -3,9 +3,11 @@ const cssToObject = require("css-to-object");
|
||||
const renderRepoCard = require("../src/renderRepoCard");
|
||||
|
||||
const { queryByTestId } = require("@testing-library/dom");
|
||||
const themes = require("../themes");
|
||||
|
||||
const data_repo = {
|
||||
repository: {
|
||||
nameWithOwner: "anuraghazra/convoychat",
|
||||
name: "convoychat",
|
||||
stargazers: { totalCount: 38000 },
|
||||
description: "Help us take over the world! React + TS + GraphQL Chat App",
|
||||
@@ -22,15 +24,16 @@ describe("Test renderRepoCard", () => {
|
||||
it("should render correctly", () => {
|
||||
document.body.innerHTML = renderRepoCard(data_repo.repository);
|
||||
|
||||
expect(document.getElementsByClassName("header")[0]).toHaveTextContent(
|
||||
"convoychat"
|
||||
);
|
||||
const [header] = document.getElementsByClassName("header");
|
||||
|
||||
expect(header).toHaveTextContent("convoychat");
|
||||
expect(header).not.toHaveTextContent("anuraghazra");
|
||||
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
|
||||
"Help us take over the world! React + TS + GraphQL Chat .."
|
||||
);
|
||||
expect(queryByTestId(document.body, "stargazers")).toHaveTextContent("38k");
|
||||
expect(queryByTestId(document.body, "forkcount")).toHaveTextContent("100");
|
||||
expect(queryByTestId(document.body, "lang")).toHaveTextContent(
|
||||
expect(queryByTestId(document.body, "lang-name")).toHaveTextContent(
|
||||
"TypeScript"
|
||||
);
|
||||
expect(queryByTestId(document.body, "lang-color")).toHaveAttribute(
|
||||
@@ -39,6 +42,15 @@ describe("Test renderRepoCard", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should display username in title (full repo name)", () => {
|
||||
document.body.innerHTML = renderRepoCard(data_repo.repository, {
|
||||
show_owner: true,
|
||||
});
|
||||
expect(document.getElementsByClassName("header")[0]).toHaveTextContent(
|
||||
"anuraghazra/convoychat"
|
||||
);
|
||||
});
|
||||
|
||||
it("should trim description", () => {
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
@@ -61,6 +73,18 @@ describe("Test renderRepoCard", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should render emojis", () => {
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
description: "This is a text with a :poop: poo emoji",
|
||||
});
|
||||
|
||||
// poop emoji may not show in all editors but it's there between "a" and "poo"
|
||||
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
|
||||
"This is a text with a 💩 poo emoji"
|
||||
);
|
||||
});
|
||||
|
||||
it("should shift the text position depending on language length", () => {
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
@@ -70,6 +94,7 @@ describe("Test renderRepoCard", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(queryByTestId(document.body, "primary-lang")).toBeInTheDocument();
|
||||
expect(document.getElementsByTagName("g")[1]).toHaveAttribute(
|
||||
"transform",
|
||||
"translate(155, 100)"
|
||||
@@ -90,6 +115,30 @@ describe("Test renderRepoCard", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should hide language if primaryLanguage is null & fallback to correct values", () => {
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
primaryLanguage: null,
|
||||
});
|
||||
|
||||
expect(queryByTestId(document.body, "primary-lang")).toBeNull();
|
||||
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
primaryLanguage: { color: null, name: null },
|
||||
});
|
||||
|
||||
expect(queryByTestId(document.body, "primary-lang")).toBeInTheDocument();
|
||||
expect(queryByTestId(document.body, "lang-color")).toHaveAttribute(
|
||||
"fill",
|
||||
"#333"
|
||||
);
|
||||
|
||||
expect(queryByTestId(document.body, "lang-name")).toHaveTextContent(
|
||||
"Unspecified"
|
||||
);
|
||||
});
|
||||
|
||||
it("should render default colors properly", () => {
|
||||
document.body.innerHTML = renderRepoCard(data_repo.repository);
|
||||
|
||||
@@ -97,13 +146,13 @@ describe("Test renderRepoCard", () => {
|
||||
const stylesObject = cssToObject(styleTag.innerHTML);
|
||||
|
||||
const headerClassStyles = stylesObject[".header"];
|
||||
const statClassStyles = stylesObject[".description"];
|
||||
const descClassStyles = stylesObject[".description"];
|
||||
const iconClassStyles = stylesObject[".icon"];
|
||||
|
||||
expect(headerClassStyles.fill).toBe("#2f80ed");
|
||||
expect(statClassStyles.fill).toBe("#333");
|
||||
expect(descClassStyles.fill).toBe("#333");
|
||||
expect(iconClassStyles.fill).toBe("#586069");
|
||||
expect(queryByTestId(document.body, "card-border")).toHaveAttribute(
|
||||
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
||||
"fill",
|
||||
"#FFFEFE"
|
||||
);
|
||||
@@ -125,15 +174,133 @@ describe("Test renderRepoCard", () => {
|
||||
const stylesObject = cssToObject(styleTag.innerHTML);
|
||||
|
||||
const headerClassStyles = stylesObject[".header"];
|
||||
const statClassStyles = stylesObject[".description"];
|
||||
const descClassStyles = stylesObject[".description"];
|
||||
const iconClassStyles = stylesObject[".icon"];
|
||||
|
||||
expect(headerClassStyles.fill).toBe(`#${customColors.title_color}`);
|
||||
expect(statClassStyles.fill).toBe(`#${customColors.text_color}`);
|
||||
expect(descClassStyles.fill).toBe(`#${customColors.text_color}`);
|
||||
expect(iconClassStyles.fill).toBe(`#${customColors.icon_color}`);
|
||||
expect(queryByTestId(document.body, "card-border")).toHaveAttribute(
|
||||
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
||||
"fill",
|
||||
"#252525"
|
||||
);
|
||||
});
|
||||
|
||||
it("should render with all the themes", () => {
|
||||
Object.keys(themes).forEach((name) => {
|
||||
document.body.innerHTML = renderRepoCard(data_repo.repository, {
|
||||
theme: name,
|
||||
});
|
||||
|
||||
const styleTag = document.querySelector("style");
|
||||
const stylesObject = cssToObject(styleTag.innerHTML);
|
||||
|
||||
const headerClassStyles = stylesObject[".header"];
|
||||
const descClassStyles = stylesObject[".description"];
|
||||
const iconClassStyles = stylesObject[".icon"];
|
||||
|
||||
expect(headerClassStyles.fill).toBe(`#${themes[name].title_color}`);
|
||||
expect(descClassStyles.fill).toBe(`#${themes[name].text_color}`);
|
||||
expect(iconClassStyles.fill).toBe(`#${themes[name].icon_color}`);
|
||||
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
||||
"fill",
|
||||
`#${themes[name].bg_color}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should render custom colors with themes", () => {
|
||||
document.body.innerHTML = renderRepoCard(data_repo.repository, {
|
||||
title_color: "5a0",
|
||||
theme: "radical",
|
||||
});
|
||||
|
||||
const styleTag = document.querySelector("style");
|
||||
const stylesObject = cssToObject(styleTag.innerHTML);
|
||||
|
||||
const headerClassStyles = stylesObject[".header"];
|
||||
const descClassStyles = stylesObject[".description"];
|
||||
const iconClassStyles = stylesObject[".icon"];
|
||||
|
||||
expect(headerClassStyles.fill).toBe("#5a0");
|
||||
expect(descClassStyles.fill).toBe(`#${themes.radical.text_color}`);
|
||||
expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`);
|
||||
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
||||
"fill",
|
||||
`#${themes.radical.bg_color}`
|
||||
);
|
||||
});
|
||||
|
||||
it("should render custom colors with themes and fallback to default colors if invalid", () => {
|
||||
document.body.innerHTML = renderRepoCard(data_repo.repository, {
|
||||
title_color: "invalid color",
|
||||
text_color: "invalid color",
|
||||
theme: "radical",
|
||||
});
|
||||
|
||||
const styleTag = document.querySelector("style");
|
||||
const stylesObject = cssToObject(styleTag.innerHTML);
|
||||
|
||||
const headerClassStyles = stylesObject[".header"];
|
||||
const descClassStyles = stylesObject[".description"];
|
||||
const iconClassStyles = stylesObject[".icon"];
|
||||
|
||||
expect(headerClassStyles.fill).toBe(`#${themes.default.title_color}`);
|
||||
expect(descClassStyles.fill).toBe(`#${themes.default.text_color}`);
|
||||
expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`);
|
||||
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
||||
"fill",
|
||||
`#${themes.radical.bg_color}`
|
||||
);
|
||||
});
|
||||
|
||||
it("should not render star count or fork count if either of the are zero", () => {
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
stargazers: { totalCount: 0 },
|
||||
});
|
||||
|
||||
expect(queryByTestId(document.body, "stargazers")).toBeNull();
|
||||
expect(queryByTestId(document.body, "forkcount")).toBeInTheDocument();
|
||||
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
stargazers: { totalCount: 1 },
|
||||
forkCount: 0,
|
||||
});
|
||||
|
||||
expect(queryByTestId(document.body, "stargazers")).toBeInTheDocument();
|
||||
expect(queryByTestId(document.body, "forkcount")).toBeNull();
|
||||
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
stargazers: { totalCount: 0 },
|
||||
forkCount: 0,
|
||||
});
|
||||
|
||||
expect(queryByTestId(document.body, "stargazers")).toBeNull();
|
||||
expect(queryByTestId(document.body, "forkcount")).toBeNull();
|
||||
});
|
||||
|
||||
it("should render badges", () => {
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
isArchived: true,
|
||||
});
|
||||
|
||||
expect(queryByTestId(document.body, "badge")).toHaveTextContent("Archived");
|
||||
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
isTemplate: true,
|
||||
});
|
||||
expect(queryByTestId(document.body, "badge")).toHaveTextContent("Template");
|
||||
});
|
||||
|
||||
it("should not render template", () => {
|
||||
document.body.innerHTML = renderRepoCard({
|
||||
...data_repo.repository,
|
||||
});
|
||||
expect(queryByTestId(document.body, "badge")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,12 @@ require("@testing-library/jest-dom");
|
||||
const cssToObject = require("css-to-object");
|
||||
const renderStatsCard = require("../src/renderStatsCard");
|
||||
|
||||
const { getByTestId, queryByTestId } = require("@testing-library/dom");
|
||||
const {
|
||||
getByTestId,
|
||||
queryByTestId,
|
||||
queryAllByTestId,
|
||||
} = require("@testing-library/dom");
|
||||
const themes = require("../themes");
|
||||
|
||||
describe("Test renderStatsCard", () => {
|
||||
const stats = {
|
||||
@@ -30,13 +35,27 @@ describe("Test renderStatsCard", () => {
|
||||
expect(getByTestId(document.body, "issues").textContent).toBe("300");
|
||||
expect(getByTestId(document.body, "prs").textContent).toBe("400");
|
||||
expect(getByTestId(document.body, "contribs").textContent).toBe("500");
|
||||
expect(queryByTestId(document.body, "card-border")).toBeInTheDocument();
|
||||
expect(queryByTestId(document.body, "card-bg")).toBeInTheDocument();
|
||||
expect(queryByTestId(document.body, "rank-circle")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should have proper name apostrophe", () => {
|
||||
document.body.innerHTML = renderStatsCard({ ...stats, name: "Anil Das" });
|
||||
|
||||
expect(document.getElementsByClassName("header")[0].textContent).toBe(
|
||||
"Anil Das' GitHub Stats"
|
||||
);
|
||||
|
||||
document.body.innerHTML = renderStatsCard({ ...stats, name: "Felix" });
|
||||
|
||||
expect(document.getElementsByClassName("header")[0].textContent).toBe(
|
||||
"Felix' GitHub Stats"
|
||||
);
|
||||
});
|
||||
|
||||
it("should hide individual stats", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
hide: "['issues', 'prs', 'contribs']",
|
||||
hide: ["issues", "prs", "contribs"],
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -52,8 +71,16 @@ describe("Test renderStatsCard", () => {
|
||||
|
||||
it("should hide_border", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, { hide_border: true });
|
||||
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
||||
"stroke-opacity",
|
||||
"0"
|
||||
);
|
||||
|
||||
expect(queryByTestId(document.body, "card-border")).not.toBeInTheDocument();
|
||||
document.body.innerHTML = renderStatsCard(stats, { hide_border: false });
|
||||
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
||||
"stroke-opacity",
|
||||
"1"
|
||||
);
|
||||
});
|
||||
|
||||
it("should hide_rank", () => {
|
||||
@@ -66,7 +93,6 @@ describe("Test renderStatsCard", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats);
|
||||
|
||||
const styleTag = document.querySelector("style");
|
||||
console.log(styleTag.textContent);
|
||||
const stylesObject = cssToObject(styleTag.textContent);
|
||||
|
||||
const headerClassStyles = stylesObject[".header"];
|
||||
@@ -76,7 +102,7 @@ describe("Test renderStatsCard", () => {
|
||||
expect(headerClassStyles.fill).toBe("#2f80ed");
|
||||
expect(statClassStyles.fill).toBe("#333");
|
||||
expect(iconClassStyles.fill).toBe("#4c71f2");
|
||||
expect(queryByTestId(document.body, "card-border")).toHaveAttribute(
|
||||
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
||||
"fill",
|
||||
"#FFFEFE"
|
||||
);
|
||||
@@ -102,9 +128,129 @@ describe("Test renderStatsCard", () => {
|
||||
expect(headerClassStyles.fill).toBe(`#${customColors.title_color}`);
|
||||
expect(statClassStyles.fill).toBe(`#${customColors.text_color}`);
|
||||
expect(iconClassStyles.fill).toBe(`#${customColors.icon_color}`);
|
||||
expect(queryByTestId(document.body, "card-border")).toHaveAttribute(
|
||||
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
||||
"fill",
|
||||
"#252525"
|
||||
);
|
||||
});
|
||||
|
||||
it("should render custom colors with themes", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
title_color: "5a0",
|
||||
theme: "radical",
|
||||
});
|
||||
|
||||
const styleTag = document.querySelector("style");
|
||||
const stylesObject = cssToObject(styleTag.innerHTML);
|
||||
|
||||
const headerClassStyles = stylesObject[".header"];
|
||||
const statClassStyles = stylesObject[".stat"];
|
||||
const iconClassStyles = stylesObject[".icon"];
|
||||
|
||||
expect(headerClassStyles.fill).toBe("#5a0");
|
||||
expect(statClassStyles.fill).toBe(`#${themes.radical.text_color}`);
|
||||
expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`);
|
||||
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
||||
"fill",
|
||||
`#${themes.radical.bg_color}`
|
||||
);
|
||||
});
|
||||
|
||||
it("should render with all the themes", () => {
|
||||
Object.keys(themes).forEach((name) => {
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
theme: name,
|
||||
});
|
||||
|
||||
const styleTag = document.querySelector("style");
|
||||
const stylesObject = cssToObject(styleTag.innerHTML);
|
||||
|
||||
const headerClassStyles = stylesObject[".header"];
|
||||
const statClassStyles = stylesObject[".stat"];
|
||||
const iconClassStyles = stylesObject[".icon"];
|
||||
|
||||
expect(headerClassStyles.fill).toBe(`#${themes[name].title_color}`);
|
||||
expect(statClassStyles.fill).toBe(`#${themes[name].text_color}`);
|
||||
expect(iconClassStyles.fill).toBe(`#${themes[name].icon_color}`);
|
||||
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
||||
"fill",
|
||||
`#${themes[name].bg_color}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should render custom colors with themes and fallback to default colors if invalid", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
title_color: "invalid color",
|
||||
text_color: "invalid color",
|
||||
theme: "radical",
|
||||
});
|
||||
|
||||
const styleTag = document.querySelector("style");
|
||||
const stylesObject = cssToObject(styleTag.innerHTML);
|
||||
|
||||
const headerClassStyles = stylesObject[".header"];
|
||||
const statClassStyles = stylesObject[".stat"];
|
||||
const iconClassStyles = stylesObject[".icon"];
|
||||
|
||||
expect(headerClassStyles.fill).toBe(`#${themes.default.title_color}`);
|
||||
expect(statClassStyles.fill).toBe(`#${themes.default.text_color}`);
|
||||
expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`);
|
||||
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
||||
"fill",
|
||||
`#${themes.radical.bg_color}`
|
||||
);
|
||||
});
|
||||
|
||||
it("should hide the title", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
hide_title: true,
|
||||
});
|
||||
|
||||
expect(document.getElementsByClassName("header")[0]).toBeUndefined();
|
||||
expect(document.getElementsByTagName("svg")[0]).toHaveAttribute(
|
||||
"height",
|
||||
"165"
|
||||
);
|
||||
expect(queryByTestId(document.body, "card-body-content")).toHaveAttribute(
|
||||
"transform",
|
||||
"translate(0, -30)"
|
||||
);
|
||||
});
|
||||
|
||||
it("should not hide the title", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, {});
|
||||
|
||||
expect(document.getElementsByClassName("header")[0]).toBeDefined();
|
||||
expect(document.getElementsByTagName("svg")[0]).toHaveAttribute(
|
||||
"height",
|
||||
"195"
|
||||
);
|
||||
expect(queryByTestId(document.body, "card-body-content")).toHaveAttribute(
|
||||
"transform",
|
||||
"translate(0, 0)"
|
||||
);
|
||||
});
|
||||
|
||||
it("should render icons correctly", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
show_icons: true,
|
||||
});
|
||||
|
||||
expect(queryAllByTestId(document.body, "icon")[0]).toBeDefined();
|
||||
expect(queryByTestId(document.body, "stars")).toBeDefined();
|
||||
expect(
|
||||
queryByTestId(document.body, "stars").previousElementSibling // the label
|
||||
).toHaveAttribute("x", "25");
|
||||
});
|
||||
|
||||
it("should not have icons if show_icons is false", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, { show_icons: false });
|
||||
|
||||
expect(queryAllByTestId(document.body, "icon")[0]).not.toBeDefined();
|
||||
expect(queryByTestId(document.body, "stars")).toBeDefined();
|
||||
expect(
|
||||
queryByTestId(document.body, "stars").previousElementSibling // the label
|
||||
).not.toHaveAttribute("x");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
require("@testing-library/jest-dom");
|
||||
const cssToObject = require("css-to-object");
|
||||
const renderTopLanguages = require("../src/renderTopLanguages");
|
||||
|
||||
const {
|
||||
getByTestId,
|
||||
queryByTestId,
|
||||
queryAllByTestId,
|
||||
} = require("@testing-library/dom");
|
||||
const themes = require("../themes");
|
||||
|
||||
describe("Test renderTopLanguages", () => {
|
||||
const langs = {
|
||||
HTML: {
|
||||
color: "#0f0",
|
||||
name: "HTML",
|
||||
size: 200,
|
||||
},
|
||||
javascript: {
|
||||
color: "#0ff",
|
||||
name: "javascript",
|
||||
size: 200,
|
||||
},
|
||||
css: {
|
||||
color: "#ff0",
|
||||
name: "css",
|
||||
size: 100,
|
||||
},
|
||||
};
|
||||
|
||||
it("should render correctly", () => {
|
||||
document.body.innerHTML = renderTopLanguages(langs);
|
||||
|
||||
expect(queryByTestId(document.body, "header")).toHaveTextContent(
|
||||
"Top Languages"
|
||||
);
|
||||
|
||||
expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent(
|
||||
"HTML"
|
||||
);
|
||||
expect(queryAllByTestId(document.body, "lang-name")[1]).toHaveTextContent(
|
||||
"javascript"
|
||||
);
|
||||
expect(queryAllByTestId(document.body, "lang-name")[2]).toHaveTextContent(
|
||||
"css"
|
||||
);
|
||||
expect(queryAllByTestId(document.body, "lang-progress")[0]).toHaveAttribute(
|
||||
"width",
|
||||
"40%"
|
||||
);
|
||||
expect(queryAllByTestId(document.body, "lang-progress")[1]).toHaveAttribute(
|
||||
"width",
|
||||
"40%"
|
||||
);
|
||||
expect(queryAllByTestId(document.body, "lang-progress")[2]).toHaveAttribute(
|
||||
"width",
|
||||
"20%"
|
||||
);
|
||||
});
|
||||
|
||||
it("should hide languages when hide is passed", () => {
|
||||
document.body.innerHTML = renderTopLanguages(langs, {
|
||||
hide: ["HTML"],
|
||||
});
|
||||
expect(queryAllByTestId(document.body, "lang-name")[0]).toBeInTheDocument(
|
||||
"javascript"
|
||||
);
|
||||
expect(queryAllByTestId(document.body, "lang-name")[1]).toBeInTheDocument(
|
||||
"css"
|
||||
);
|
||||
expect(queryAllByTestId(document.body, "lang-name")[2]).not.toBeDefined();
|
||||
|
||||
// multiple languages passed
|
||||
document.body.innerHTML = renderTopLanguages(langs, {
|
||||
hide: ["HTML","css"],
|
||||
});
|
||||
expect(queryAllByTestId(document.body, "lang-name")[0]).toBeInTheDocument(
|
||||
"javascript"
|
||||
);
|
||||
expect(queryAllByTestId(document.body, "lang-name")[1]).not.toBeDefined();
|
||||
});
|
||||
|
||||
it("should resize the height correctly depending on langs", () => {
|
||||
document.body.innerHTML = renderTopLanguages(langs, {});
|
||||
expect(document.querySelector("svg")).toHaveAttribute("height", "205");
|
||||
|
||||
document.body.innerHTML = renderTopLanguages(
|
||||
{
|
||||
...langs,
|
||||
python: {
|
||||
color: "#ff0",
|
||||
name: "python",
|
||||
size: 100,
|
||||
},
|
||||
},
|
||||
{}
|
||||
);
|
||||
expect(document.querySelector("svg")).toHaveAttribute("height", "245");
|
||||
});
|
||||
|
||||
it("should hide_title", () => {
|
||||
document.body.innerHTML = renderTopLanguages(langs, { hide_title: false });
|
||||
expect(document.querySelector("svg")).toHaveAttribute("height", "205");
|
||||
expect(queryByTestId(document.body, "lang-items")).toHaveAttribute(
|
||||
"y",
|
||||
"55"
|
||||
);
|
||||
|
||||
// Lets hide now
|
||||
document.body.innerHTML = renderTopLanguages(langs, { hide_title: true });
|
||||
expect(document.querySelector("svg")).toHaveAttribute("height", "175");
|
||||
|
||||
expect(queryByTestId(document.body, "header")).not.toBeInTheDocument();
|
||||
expect(queryByTestId(document.body, "lang-items")).toHaveAttribute(
|
||||
"y",
|
||||
"25"
|
||||
);
|
||||
});
|
||||
|
||||
it("should render with custom width set", () => {
|
||||
document.body.innerHTML = renderTopLanguages(langs, {});
|
||||
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "300");
|
||||
|
||||
document.body.innerHTML = renderTopLanguages(langs, { card_width: 400 });
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "400");
|
||||
});
|
||||
|
||||
it("should render default colors properly", () => {
|
||||
document.body.innerHTML = renderTopLanguages(langs);
|
||||
|
||||
const styleTag = document.querySelector("style");
|
||||
const stylesObject = cssToObject(styleTag.textContent);
|
||||
|
||||
const headerStyles = stylesObject[".header"];
|
||||
const langNameStyles = stylesObject[".lang-name"];
|
||||
|
||||
expect(headerStyles.fill).toBe("#2f80ed");
|
||||
expect(langNameStyles.fill).toBe("#333");
|
||||
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
||||
"fill",
|
||||
"#FFFEFE"
|
||||
);
|
||||
});
|
||||
|
||||
it("should render custom colors properly", () => {
|
||||
const customColors = {
|
||||
title_color: "5a0",
|
||||
icon_color: "1b998b",
|
||||
text_color: "9991",
|
||||
bg_color: "252525",
|
||||
};
|
||||
|
||||
document.body.innerHTML = renderTopLanguages(langs, { ...customColors });
|
||||
|
||||
const styleTag = document.querySelector("style");
|
||||
const stylesObject = cssToObject(styleTag.innerHTML);
|
||||
|
||||
const headerStyles = stylesObject[".header"];
|
||||
const langNameStyles = stylesObject[".lang-name"];
|
||||
|
||||
expect(headerStyles.fill).toBe(`#${customColors.title_color}`);
|
||||
expect(langNameStyles.fill).toBe(`#${customColors.text_color}`);
|
||||
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
||||
"fill",
|
||||
"#252525"
|
||||
);
|
||||
});
|
||||
|
||||
it("should render custom colors with themes", () => {
|
||||
document.body.innerHTML = renderTopLanguages(langs, {
|
||||
title_color: "5a0",
|
||||
theme: "radical",
|
||||
});
|
||||
|
||||
const styleTag = document.querySelector("style");
|
||||
const stylesObject = cssToObject(styleTag.innerHTML);
|
||||
|
||||
const headerStyles = stylesObject[".header"];
|
||||
const langNameStyles = stylesObject[".lang-name"];
|
||||
|
||||
expect(headerStyles.fill).toBe("#5a0");
|
||||
expect(langNameStyles.fill).toBe(`#${themes.radical.text_color}`);
|
||||
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
||||
"fill",
|
||||
`#${themes.radical.bg_color}`
|
||||
);
|
||||
});
|
||||
|
||||
it("should render with all the themes", () => {
|
||||
Object.keys(themes).forEach((name) => {
|
||||
document.body.innerHTML = renderTopLanguages(langs, {
|
||||
theme: name,
|
||||
});
|
||||
|
||||
const styleTag = document.querySelector("style");
|
||||
const stylesObject = cssToObject(styleTag.innerHTML);
|
||||
|
||||
const headerStyles = stylesObject[".header"];
|
||||
const langNameStyles = stylesObject[".lang-name"];
|
||||
|
||||
expect(headerStyles.fill).toBe(`#${themes[name].title_color}`);
|
||||
expect(langNameStyles.fill).toBe(`#${themes[name].text_color}`);
|
||||
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
||||
"fill",
|
||||
`#${themes[name].bg_color}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
require("@testing-library/jest-dom");
|
||||
const retryer = require("../src/retryer");
|
||||
const { logger } = require("../src/utils");
|
||||
|
||||
const fetcher = jest.fn((variables, token) => {
|
||||
logger.log(variables, token);
|
||||
return new Promise((res, rej) => res({ data: "ok" }));
|
||||
});
|
||||
|
||||
const fetcherFail = jest.fn(() => {
|
||||
return new Promise((res, rej) =>
|
||||
res({ data: { errors: [{ type: "RATE_LIMITED" }] } })
|
||||
);
|
||||
});
|
||||
|
||||
const fetcherFailOnSecondTry = jest.fn((_vars, _token, retries) => {
|
||||
return new Promise((res, rej) => {
|
||||
// faking rate limit
|
||||
if (retries < 1) {
|
||||
return res({ data: { errors: [{ type: "RATE_LIMITED" }] } });
|
||||
}
|
||||
return res({ data: "ok" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test Retryer", () => {
|
||||
it("retryer should return value and have zero retries on first try", async () => {
|
||||
let res = await retryer(fetcher, {});
|
||||
|
||||
expect(fetcher).toBeCalledTimes(1);
|
||||
expect(res).toStrictEqual({ data: "ok" });
|
||||
});
|
||||
|
||||
it("retryer should return value and have 2 retries", async () => {
|
||||
let res = await retryer(fetcherFailOnSecondTry, {});
|
||||
|
||||
expect(fetcherFailOnSecondTry).toBeCalledTimes(2);
|
||||
expect(res).toStrictEqual({ data: "ok" });
|
||||
});
|
||||
|
||||
it("retryer should throw error if maximum retries reached", async () => {
|
||||
let res;
|
||||
|
||||
try {
|
||||
res = await retryer(fetcherFail, {});
|
||||
} catch (err) {
|
||||
expect(fetcherFail).toBeCalledTimes(8);
|
||||
expect(err.message).toBe("Maximum retries exceeded");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
require("@testing-library/jest-dom");
|
||||
const axios = require("axios");
|
||||
const MockAdapter = require("axios-mock-adapter");
|
||||
const topLangs = require("../api/top-langs");
|
||||
const renderTopLanguages = require("../src/renderTopLanguages");
|
||||
const { renderError } = require("../src/utils");
|
||||
|
||||
const data_langs = {
|
||||
data: {
|
||||
user: {
|
||||
repositories: {
|
||||
nodes: [
|
||||
{
|
||||
languages: {
|
||||
edges: [{ size: 100, node: { color: "#0f0", name: "HTML" } }],
|
||||
},
|
||||
},
|
||||
{
|
||||
languages: {
|
||||
edges: [{ size: 100, node: { color: "#0f0", name: "HTML" } }],
|
||||
},
|
||||
},
|
||||
{
|
||||
languages: {
|
||||
edges: [
|
||||
{ size: 100, node: { color: "#0ff", name: "javascript" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
languages: {
|
||||
edges: [
|
||||
{ size: 100, node: { color: "#0ff", name: "javascript" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const error = {
|
||||
errors: [
|
||||
{
|
||||
type: "NOT_FOUND",
|
||||
path: ["user"],
|
||||
locations: [],
|
||||
message: "Could not fetch user",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const langs = {
|
||||
HTML: {
|
||||
color: "#0f0",
|
||||
name: "HTML",
|
||||
size: 200,
|
||||
},
|
||||
javascript: {
|
||||
color: "#0ff",
|
||||
name: "javascript",
|
||||
size: 200,
|
||||
},
|
||||
};
|
||||
|
||||
const mock = new MockAdapter(axios);
|
||||
|
||||
afterEach(() => {
|
||||
mock.reset();
|
||||
});
|
||||
|
||||
describe("Test /api/top-langs", () => {
|
||||
it("should test the request", async () => {
|
||||
const req = {
|
||||
query: {
|
||||
username: "anuraghazra",
|
||||
},
|
||||
};
|
||||
const res = {
|
||||
setHeader: jest.fn(),
|
||||
send: jest.fn(),
|
||||
};
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
|
||||
|
||||
await topLangs(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
|
||||
expect(res.send).toBeCalledWith(renderTopLanguages(langs));
|
||||
});
|
||||
|
||||
it("should work with the query options", async () => {
|
||||
const req = {
|
||||
query: {
|
||||
username: "anuraghazra",
|
||||
hide_title: true,
|
||||
card_width: 100,
|
||||
title_color: "fff",
|
||||
icon_color: "fff",
|
||||
text_color: "fff",
|
||||
bg_color: "fff",
|
||||
},
|
||||
};
|
||||
const res = {
|
||||
setHeader: jest.fn(),
|
||||
send: jest.fn(),
|
||||
};
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
|
||||
|
||||
await topLangs(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
|
||||
expect(res.send).toBeCalledWith(
|
||||
renderTopLanguages(langs, {
|
||||
hide_title: true,
|
||||
card_width: 100,
|
||||
title_color: "fff",
|
||||
icon_color: "fff",
|
||||
text_color: "fff",
|
||||
bg_color: "fff",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should render error card on error", async () => {
|
||||
const req = {
|
||||
query: {
|
||||
username: "anuraghazra",
|
||||
},
|
||||
};
|
||||
const res = {
|
||||
setHeader: jest.fn(),
|
||||
send: jest.fn(),
|
||||
};
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, error);
|
||||
|
||||
await topLangs(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
|
||||
expect(res.send).toBeCalledWith(renderError(error.errors[0].message));
|
||||
});
|
||||
});
|
||||
+87
-3
@@ -1,4 +1,13 @@
|
||||
const { kFormatter, encodeHTML, renderError } = require("../src/utils");
|
||||
require("@testing-library/jest-dom");
|
||||
const {
|
||||
kFormatter,
|
||||
encodeHTML,
|
||||
renderError,
|
||||
FlexLayout,
|
||||
getCardColors,
|
||||
} = require("../src/utils");
|
||||
|
||||
const { queryByTestId } = require("@testing-library/dom");
|
||||
|
||||
describe("Test utils.js", () => {
|
||||
it("should test kFormatter", () => {
|
||||
@@ -19,8 +28,83 @@ describe("Test utils.js", () => {
|
||||
|
||||
it("should test renderError", () => {
|
||||
document.body.innerHTML = renderError("Something went wrong");
|
||||
expect(document.getElementById("message").textContent).toBe(
|
||||
"Something went wrong"
|
||||
expect(
|
||||
queryByTestId(document.body, "message").children[0]
|
||||
).toHaveTextContent(/Something went wrong/gim);
|
||||
expect(queryByTestId(document.body, "message").children[1]).toBeEmpty(2);
|
||||
|
||||
// Secondary message
|
||||
document.body.innerHTML = renderError(
|
||||
"Something went wrong",
|
||||
"Secondary Message"
|
||||
);
|
||||
expect(
|
||||
queryByTestId(document.body, "message").children[1]
|
||||
).toHaveTextContent(/Secondary Message/gim);
|
||||
});
|
||||
|
||||
it("should test FlexLayout", () => {
|
||||
const layout = FlexLayout({
|
||||
items: ["<text>1</text>", "<text>2</text>"],
|
||||
gap: 60,
|
||||
}).join("");
|
||||
|
||||
expect(layout).toBe(
|
||||
`<g transform=\"translate(0, 0)\"><text>1</text></g><g transform=\"translate(60, 0)\"><text>2</text></g>`
|
||||
);
|
||||
|
||||
const columns = FlexLayout({
|
||||
items: ["<text>1</text>", "<text>2</text>"],
|
||||
gap: 60,
|
||||
direction: "column",
|
||||
}).join("");
|
||||
|
||||
expect(columns).toBe(
|
||||
`<g transform=\"translate(0, 0)\"><text>1</text></g><g transform=\"translate(0, 60)\"><text>2</text></g>`
|
||||
);
|
||||
});
|
||||
|
||||
it("getCardColors: should return expected values", () => {
|
||||
let colors = getCardColors({
|
||||
title_color: "f00",
|
||||
text_color: "0f0",
|
||||
icon_color: "00f",
|
||||
bg_color: "fff",
|
||||
theme: "dark",
|
||||
});
|
||||
expect(colors).toStrictEqual({
|
||||
titleColor: "#f00",
|
||||
textColor: "#0f0",
|
||||
iconColor: "#00f",
|
||||
bgColor: "#fff",
|
||||
});
|
||||
});
|
||||
|
||||
it("getCardColors: should fallback to default colors if color is invalid", () => {
|
||||
let colors = getCardColors({
|
||||
title_color: "invalidcolor",
|
||||
text_color: "0f0",
|
||||
icon_color: "00f",
|
||||
bg_color: "fff",
|
||||
theme: "dark",
|
||||
});
|
||||
expect(colors).toStrictEqual({
|
||||
titleColor: "#2f80ed",
|
||||
textColor: "#0f0",
|
||||
iconColor: "#00f",
|
||||
bgColor: "#fff",
|
||||
});
|
||||
});
|
||||
|
||||
it("getCardColors: should fallback to specified theme colors if is not defined", () => {
|
||||
let colors = getCardColors({
|
||||
theme: "dark",
|
||||
});
|
||||
expect(colors).toStrictEqual({
|
||||
titleColor: "#fff",
|
||||
textColor: "#9f9f9f",
|
||||
iconColor: "#79ff97",
|
||||
bgColor: "#151515",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
## Available Themes
|
||||
|
||||
<!-- DO NOT EDIT THIS FILE DIRECTLY -->
|
||||
|
||||
With inbuilt themes you can customize the look of the card without doing any manual customization.
|
||||
|
||||
Use `?theme=THEME_NAME` parameter like so :-
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
## Stats
|
||||
|
||||
> These themes work both for the Stats Card and Repo Card.
|
||||
|
||||
| | | |
|
||||
| :--: | :--: | :--: |
|
||||
| `default` ![default][default] | `dark` ![dark][dark] | `radical` ![radical][radical] |
|
||||
| `merko` ![merko][merko] | `gruvbox` ![gruvbox][gruvbox] | `tokyonight` ![tokyonight][tokyonight] |
|
||||
| `onedark` ![onedark][onedark] | `cobalt` ![cobalt][cobalt] | `synthwave` ![synthwave][synthwave] |
|
||||
| `highcontrast` ![highcontrast][highcontrast] | `dracula` ![dracula][dracula] | `prussian` ![prussian][prussian] |
|
||||
| `monokai` ![monokai][monokai] | `vue` ![vue][vue] | `shades-of-purple` ![shades-of-purple][shades-of-purple] |
|
||||
| `nightowl` ![nightowl][nightowl] | `buefy` ![buefy][buefy] | [Add your theme][add-theme] |
|
||||
|
||||
## Repo Card
|
||||
|
||||
> These themes work both for the Stats Card and Repo Card.
|
||||
|
||||
| | | |
|
||||
| :--: | :--: | :--: |
|
||||
| `default_repocard` ![default_repocard][default_repocard_repo] | `dark` ![dark][dark_repo] | `radical` ![radical][radical_repo] |
|
||||
| `merko` ![merko][merko_repo] | `gruvbox` ![gruvbox][gruvbox_repo] | `tokyonight` ![tokyonight][tokyonight_repo] |
|
||||
| `onedark` ![onedark][onedark_repo] | `cobalt` ![cobalt][cobalt_repo] | `synthwave` ![synthwave][synthwave_repo] |
|
||||
| `highcontrast` ![highcontrast][highcontrast_repo] | `dracula` ![dracula][dracula_repo] | `prussian` ![prussian][prussian_repo] |
|
||||
| `monokai` ![monokai][monokai_repo] | `vue` ![vue][vue_repo] | `shades-of-purple` ![shades-of-purple][shades-of-purple_repo] |
|
||||
| `nightowl` ![nightowl][nightowl_repo] | `buefy` ![buefy][buefy_repo] | [Add your theme][add-theme] |
|
||||
|
||||
|
||||
[default]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=default
|
||||
[default_repocard]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=default_repocard
|
||||
[dark]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=dark
|
||||
[radical]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=radical
|
||||
[merko]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=merko
|
||||
[gruvbox]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=gruvbox
|
||||
[tokyonight]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=tokyonight
|
||||
[onedark]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=onedark
|
||||
[cobalt]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=cobalt
|
||||
[synthwave]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=synthwave
|
||||
[highcontrast]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=highcontrast
|
||||
[dracula]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=dracula
|
||||
[prussian]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=prussian
|
||||
[monokai]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=monokai
|
||||
[vue]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=vue
|
||||
[shades-of-purple]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=shades-of-purple
|
||||
[nightowl]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=nightowl
|
||||
[buefy]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=buefy
|
||||
|
||||
|
||||
[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
|
||||
[dark_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=dark
|
||||
[radical_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=radical
|
||||
[merko_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=merko
|
||||
[gruvbox_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=gruvbox
|
||||
[tokyonight_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=tokyonight
|
||||
[onedark_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=onedark
|
||||
[cobalt_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=cobalt
|
||||
[synthwave_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=synthwave
|
||||
[highcontrast_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=highcontrast
|
||||
[dracula_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=dracula
|
||||
[prussian_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=prussian
|
||||
[monokai_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=monokai
|
||||
[vue_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=vue
|
||||
[shades-of-purple_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=shades-of-purple
|
||||
[nightowl_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=nightowl
|
||||
[buefy_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=buefy
|
||||
|
||||
|
||||
[add-theme]: https://github.com/anuraghazra/github-readme-stats/edit/master/themes/index.js
|
||||
|
||||
Wanted to add a new theme? Consider reading the [contribution guidelines](../CONTRIBUTING.md#themes-contribution) :D
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
const themes = {
|
||||
default: {
|
||||
title_color: "2f80ed",
|
||||
icon_color: "4c71f2",
|
||||
text_color: "333",
|
||||
bg_color: "FFFEFE",
|
||||
},
|
||||
default_repocard: {
|
||||
title_color: "2f80ed",
|
||||
icon_color: "586069", // icon color is different
|
||||
text_color: "333",
|
||||
bg_color: "FFFEFE",
|
||||
},
|
||||
dark: {
|
||||
title_color: "fff",
|
||||
icon_color: "79ff97",
|
||||
text_color: "9f9f9f",
|
||||
bg_color: "151515",
|
||||
},
|
||||
radical: {
|
||||
title_color: "fe428e",
|
||||
icon_color: "f8d847",
|
||||
text_color: "a9fef7",
|
||||
bg_color: "141321",
|
||||
},
|
||||
merko: {
|
||||
title_color: "abd200",
|
||||
icon_color: "b7d364",
|
||||
text_color: "68b587",
|
||||
bg_color: "0a0f0b",
|
||||
},
|
||||
gruvbox: {
|
||||
title_color: "fabd2f",
|
||||
icon_color: "fe8019",
|
||||
text_color: "8ec07c",
|
||||
bg_color: "282828",
|
||||
},
|
||||
tokyonight: {
|
||||
title_color: "70a5fd",
|
||||
icon_color: "bf91f3",
|
||||
text_color: "38bdae",
|
||||
bg_color: "1a1b27",
|
||||
},
|
||||
onedark: {
|
||||
title_color: "e4bf7a",
|
||||
icon_color: "8eb573",
|
||||
text_color: "df6d74",
|
||||
bg_color: "282c34",
|
||||
},
|
||||
cobalt: {
|
||||
title_color: "e683d9",
|
||||
icon_color: "0480ef",
|
||||
text_color: "75eeb2",
|
||||
bg_color: "193549",
|
||||
},
|
||||
synthwave: {
|
||||
title_color: "e2e9ec",
|
||||
icon_color: "ef8539",
|
||||
text_color: "e5289e",
|
||||
bg_color: "2b213a",
|
||||
},
|
||||
highcontrast: {
|
||||
title_color: "e7f216",
|
||||
icon_color: "00ffff",
|
||||
text_color: "fff",
|
||||
bg_color: "000",
|
||||
},
|
||||
dracula: {
|
||||
title_color: "ff6e96",
|
||||
icon_color: "79dafa",
|
||||
text_color: "f8f8f2",
|
||||
bg_color: "282a36",
|
||||
},
|
||||
prussian: {
|
||||
title_color: "bddfff",
|
||||
icon_color: "38a0ff",
|
||||
text_color: "6e93b5",
|
||||
bg_color: "172f45",
|
||||
},
|
||||
monokai: {
|
||||
title_color: "eb1f6a",
|
||||
icon_color: "e28905",
|
||||
text_color: "f1f1eb",
|
||||
bg_color: "272822",
|
||||
},
|
||||
vue: {
|
||||
title_color: "41b883",
|
||||
icon_color: "41b883",
|
||||
text_color: "273849",
|
||||
bg_color: "FFFEFE",
|
||||
},
|
||||
"shades-of-purple": {
|
||||
title_color: "FAD000",
|
||||
icon_color: "B362FF",
|
||||
text_color: "A599E9",
|
||||
bg_color: "2D2B55",
|
||||
},
|
||||
nightowl: {
|
||||
title_color: "C792EA",
|
||||
icon_color: "FFEB95",
|
||||
text_color: "7FDBCA",
|
||||
bg_color: "011627",
|
||||
},
|
||||
buefy: {
|
||||
title_color: "7957d5",
|
||||
icon_color: "ff3860",
|
||||
text_color: "363636",
|
||||
bg_color: "ffffff",
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = themes;
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/",
|
||||
"destination": "https://github.com/anuraghazra/github-readme-stats"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user