├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── generate-theme-doc.yml
│ └── test.yml
├── .gitignore
├── .vercelignore
├── CONTRIBUTING.md
├── LICENSE
├── api
├── index.js
├── pin.js
└── top-langs.js
├── codecov.yml
├── jest.config.js
├── package.json
├── readme.md
├── readme_cn.md
├── readme_de.md
├── readme_es.md
├── readme_ja.md
├── scripts
├── generate-theme-doc.js
└── push-theme-readme.sh
├── src
├── calculateRank.js
├── fetchRepo.js
├── fetchStats.js
├── fetchTopLanguages.js
├── getStyles.js
├── icons.js
├── renderRepoCard.js
├── renderStatsCard.js
├── renderTopLanguages.js
├── retryer.js
└── utils.js
├── tests
├── api.test.js
├── calculateRank.test.js
├── fetchRepo.test.js
├── fetchStats.test.js
├── fetchTopLanguages.test.js
├── pin.test.js
├── renderRepoCard.test.js
├── renderStatsCard.test.js
├── renderTopLanguages.test.js
├── retryer.test.js
├── top-langs.test.js
└── utils.test.js
├── themes
├── README.md
└── index.js
└── vercel.json
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: ["https://www.paypal.me/anuraghazra", "https://www.buymeacoffee.com/anuraghazra"] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **Expected behavior**
14 | A clear and concise description of what you expected to happen.
15 |
16 | **Screenshots / Live demo link (paste the github-readme-stats link as markdown image)**
17 | If applicable, add screenshots to help explain your problem.
18 |
19 | **Additional context**
20 | Add any other context about the problem here.
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/generate-theme-doc.yml:
--------------------------------------------------------------------------------
1 | name: Generate Theme Readme
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | paths:
8 | - "themes/index.js"
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v1
16 | - name: setup node
17 | uses: actions/setup-node@v1
18 | with:
19 | node-version: "12.x"
20 |
21 | - name: npm install, generate readme
22 | run: |
23 | npm install
24 | npm run theme-readme-gen
25 | env:
26 | CI: true
27 |
28 | - name: Run Script
29 | uses: skx/github-action-tester@master
30 | with:
31 | script: ./scripts/push-theme-readme.sh
32 | env:
33 | CI: true
34 | PERSONAL_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
35 | GH_REPO: ${{ secrets.GH_REPO }}
36 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - "*"
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 |
18 | - name: Setup Node
19 | uses: actions/setup-node@v1
20 | with:
21 | node-version: "12.x"
22 |
23 | - name: Cache node modules
24 | uses: actions/cache@v2
25 | env:
26 | cache-name: cache-node-modules
27 | with:
28 | path: ~/.npm
29 | key: ${{ runner.os }}-npm-cache-${{ hashFiles('**/package-lock.json') }}
30 | restore-keys: |
31 | ${{ runner.os }}-npm-cache-
32 |
33 | - name: Install & Test
34 | run: |
35 | npm install
36 | npm run test
37 |
38 | - name: Code Coverage
39 | uses: codecov/codecov-action@v1
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vercel
2 | .env
3 | node_modules
4 | package-lock.json
5 | *.lock
6 | .vscode/
7 | coverage
8 |
--------------------------------------------------------------------------------
/.vercelignore:
--------------------------------------------------------------------------------
1 | .env
2 | package-lock.json
3 | coverage
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to [github-readme-stats](https://github.com/anuraghazra/github-readme-stats)
2 |
3 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
4 |
5 | - Reporting a issue
6 | - Discussing the current state of the code
7 | - Submitting a fix
8 | - Proposing new features
9 | - Becoming a maintainer
10 |
11 | ## All Changes Happen Through Pull Requests
12 |
13 | Pull requests are the best way to propose changes. We actively welcome your pull requests:
14 |
15 | 1. Fork the repo and create your branch from `master`.
16 | 1. If you've added code that should be tested, add some tests' example.
17 | 1. If you've changed APIs, update the documentation.
18 | 1. Issue that pull request!
19 |
20 | ## Local Development
21 |
22 | To run & test github-readme-stats you need to follow few simple steps :-
23 | _(make sure you already have a [vercel](https://vercel.com/) account)_
24 |
25 | 1. Install [Vercel CLI](https://vercel.com/download)
26 | 1. Fork the repository and clone the code to your local machine
27 | 1. Run the command "vercel" in the root and follow the steps there
28 | 1. Create a `.env` file in the root of the directory
29 | 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)
30 | 1. Run the command "vercel dev" to start a development server at https://localhost:3000
31 |
32 | ## Themes Contribution
33 |
34 | GitHub Readme Stats supports custom theming and you can also contribute new themes!
35 |
36 | All you need to do is edit [themes/index.js](./themes/index.js) file and add your theme at the end of the file.
37 |
38 | 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`
39 |
40 | > 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.
41 |
42 | ## Any contributions you make will be under the MIT Software License
43 |
44 | 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.
45 |
46 | ## Report issues/bugs using GitHub's [issues](https://github.com/anuraghazra/github-readme-stats/issues)
47 |
48 | 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!
49 |
50 | ### Bug Reports
51 |
52 | **Great Bug Reports** tend to have:
53 |
54 | - A quick summary and/or background
55 | - Steps to reproduce
56 | - Be specific!
57 | - Share the snapshot, if possible.
58 | - GitHub Readme Stats' live link
59 | - What actually happens
60 | - What you expected would happen
61 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
62 |
63 | People _love_ thorough bug reports. I'm not even kidding.
64 |
65 | ### Feature Request
66 |
67 | **Great Feature Requests** tend to have:
68 |
69 | - A quick idea summary
70 | - What & why you wanted to add the specific feature
71 | - Additional Context like images, links to resources to implement the feature etc etc.
72 |
73 | ## License
74 |
75 | By contributing, you agree that your contributions will be licensed under its [MIT License](./LICENSE).
76 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Anurag Hazra
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/api/index.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const {
3 | renderError,
4 | parseBoolean,
5 | parseArray,
6 | clampValue,
7 | CONSTANTS,
8 | } = require("../src/utils");
9 | const fetchStats = require("../src/fetchStats");
10 | const renderStatsCard = require("../src/renderStatsCard");
11 |
12 | module.exports = async (req, res) => {
13 | const {
14 | username,
15 | hide,
16 | hide_title,
17 | hide_border,
18 | hide_rank,
19 | show_icons,
20 | count_private,
21 | line_height,
22 | title_color,
23 | icon_color,
24 | text_color,
25 | bg_color,
26 | theme,
27 | cache_seconds,
28 | } = req.query;
29 | let stats;
30 |
31 | res.setHeader("Content-Type", "image/svg+xml");
32 |
33 | try {
34 | stats = await fetchStats(username, parseBoolean(count_private));
35 | } catch (err) {
36 | return res.send(
37 | renderError(
38 | err.message,
39 | "Make sure the provided username is not an organization"
40 | )
41 | );
42 | }
43 |
44 | const cacheSeconds = clampValue(
45 | parseInt(cache_seconds || CONSTANTS.THIRTY_MINUTES, 10),
46 | CONSTANTS.THIRTY_MINUTES,
47 | CONSTANTS.ONE_DAY
48 | );
49 |
50 | res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`);
51 |
52 | res.send(
53 | renderStatsCard(stats, {
54 | hide: parseArray(hide),
55 | show_icons: parseBoolean(show_icons),
56 | hide_title: parseBoolean(hide_title),
57 | hide_border: parseBoolean(hide_border),
58 | hide_rank: parseBoolean(hide_rank),
59 | line_height,
60 | title_color,
61 | icon_color,
62 | text_color,
63 | bg_color,
64 | theme,
65 | })
66 | );
67 | };
68 |
--------------------------------------------------------------------------------
/api/pin.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const {
3 | renderError,
4 | parseBoolean,
5 | clampValue,
6 | CONSTANTS,
7 | logger,
8 | } = require("../src/utils");
9 | const fetchRepo = require("../src/fetchRepo");
10 | const renderRepoCard = require("../src/renderRepoCard");
11 |
12 | module.exports = async (req, res) => {
13 | const {
14 | username,
15 | repo,
16 | title_color,
17 | icon_color,
18 | text_color,
19 | bg_color,
20 | theme,
21 | show_owner,
22 | cache_seconds,
23 | } = req.query;
24 |
25 | let repoData;
26 |
27 | res.setHeader("Content-Type", "image/svg+xml");
28 |
29 | try {
30 | repoData = await fetchRepo(username, repo);
31 | } catch (err) {
32 | logger.error(err);
33 | return res.send(renderError(err.message));
34 | }
35 |
36 | let cacheSeconds = clampValue(
37 | parseInt(cache_seconds || CONSTANTS.THIRTY_MINUTES, 10),
38 | CONSTANTS.THIRTY_MINUTES,
39 | CONSTANTS.ONE_DAY
40 | );
41 |
42 | /*
43 | if star count & fork count is over 1k then we are kFormating the text
44 | and if both are zero we are not showing the stats
45 | so we can just make the cache longer, since there is no need to frequent updates
46 | */
47 | const stars = repoData.stargazers.totalCount;
48 | const forks = repoData.forkCount;
49 | const isBothOver1K = stars > 1000 && forks > 1000;
50 | const isBothUnder1 = stars < 1 && forks < 1;
51 | if (!cache_seconds && (isBothOver1K || isBothUnder1)) {
52 | cacheSeconds = CONSTANTS.TWO_HOURS;
53 | }
54 |
55 | res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`);
56 |
57 | res.send(
58 | renderRepoCard(repoData, {
59 | title_color,
60 | icon_color,
61 | text_color,
62 | bg_color,
63 | theme,
64 | show_owner: parseBoolean(show_owner),
65 | })
66 | );
67 | };
68 |
--------------------------------------------------------------------------------
/api/top-langs.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const {
3 | renderError,
4 | clampValue,
5 | parseBoolean,
6 | parseArray,
7 | CONSTANTS,
8 | } = require("../src/utils");
9 | const fetchTopLanguages = require("../src/fetchTopLanguages");
10 | const renderTopLanguages = require("../src/renderTopLanguages");
11 |
12 | module.exports = async (req, res) => {
13 | const {
14 | username,
15 | hide,
16 | hide_title,
17 | card_width,
18 | title_color,
19 | text_color,
20 | bg_color,
21 | theme,
22 | cache_seconds,
23 | layout
24 | } = req.query;
25 | let topLangs;
26 |
27 | res.setHeader("Content-Type", "image/svg+xml");
28 |
29 | try {
30 | topLangs = await fetchTopLanguages(username);
31 | } catch (err) {
32 | return res.send(renderError(err.message));
33 | }
34 |
35 | const cacheSeconds = clampValue(
36 | parseInt(cache_seconds || CONSTANTS.THIRTY_MINUTES, 10),
37 | CONSTANTS.THIRTY_MINUTES,
38 | CONSTANTS.ONE_DAY
39 | );
40 |
41 | res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`);
42 |
43 | res.send(
44 | renderTopLanguages(topLangs, {
45 | theme,
46 | hide_title: parseBoolean(hide_title),
47 | card_width: parseInt(card_width, 10),
48 | hide: parseArray(hide),
49 | title_color,
50 | text_color,
51 | bg_color,
52 | theme,
53 | layout
54 | })
55 | );
56 | };
57 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | codecov:
2 | require_ci_to_pass: yes
3 |
4 | coverage:
5 | precision: 2
6 | round: down
7 | range: "70...100"
8 |
9 | status:
10 | project:
11 | default:
12 | threshold: 5
13 | patch: false
14 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | clearMocks: true,
3 | };
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "github-readme-stats",
3 | "version": "1.0.0",
4 | "description": "Dynamically generate stats for your github readmes",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "jest --coverage",
8 | "test:watch": "jest --watch",
9 | "theme-readme-gen": "node scripts/generate-theme-doc"
10 | },
11 | "author": "Anurag Hazra",
12 | "license": "MIT",
13 | "devDependencies": {
14 | "@testing-library/dom": "^7.20.0",
15 | "@testing-library/jest-dom": "^5.11.0",
16 | "axios": "^0.19.2",
17 | "axios-mock-adapter": "^1.18.1",
18 | "css-to-object": "^1.1.0",
19 | "husky": "^4.2.5",
20 | "jest": "^26.1.0"
21 | },
22 | "dependencies": {
23 | "dotenv": "^8.2.0",
24 | "emoji-name-map": "^1.2.8",
25 | "word-wrap": "^1.2.3"
26 | },
27 | "husky": {
28 | "hooks": {
29 | "pre-commit": "npm test"
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
GitHub Readme Stats
4 | Get dynamically generated GitHub stats on your readmes!
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | View Demo
31 | ·
32 | Report Bug
33 | ·
34 | Request Feature
35 |
36 |
37 | 简体中文
38 | ·
39 | Español
40 | ·
41 | Deutsch
42 | ·
43 | 日本語
44 |
45 |
46 | Loved the project? Please consider donating to help it improve!
47 |
48 | # Features
49 |
50 | - [GitHub Stats Card](#github-stats-card)
51 | - [GitHub Extra Pins](#github-extra-pins)
52 | - [Top Languages Card](#top-languages-card)
53 | - [Themes](#themes)
54 | - [Customization](#customization)
55 | - [Deploy Yourself](#deploy-on-your-own-vercel-instance)
56 |
57 | # GitHub Stats Card
58 |
59 | Copy paste this into your markdown content, and that's it. Simple!
60 |
61 | Change the `?username=` value to your GitHub's username.
62 |
63 | ```md
64 | [](https://github.com/anuraghazra/github-readme-stats)
65 | ```
66 |
67 | _Note: Ranks are calculated based on user's stats, see [src/calculateRank.js](./src/calculateRank.js)_
68 |
69 | ### Hiding individual stats
70 |
71 | To hide any specific stats, you can pass a query parameter `?hide=` with comma separated values.
72 |
73 | > Options: `&hide=stars,commits,prs,issues,contribs`
74 |
75 | ```md
76 | 
77 | ```
78 |
79 | ### Adding private contributions count to total commits count
80 |
81 | You can add the count of all your private contributions to the total commits count by using the query parameter `?count_private=true`.
82 |
83 | _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._
84 |
85 | > Options: `&count_private=true`
86 |
87 | ```md
88 | 
89 | ```
90 |
91 | ### Showing icons
92 |
93 | To enable icons, you can pass `show_icons=true` in the query param, like so:
94 |
95 | ```md
96 | 
97 | ```
98 |
99 | ### Themes
100 |
101 | With inbuilt themes you can customize the look of the card without doing any [manual customization](#customization).
102 |
103 | Use `?theme=THEME_NAME` parameter like so :-
104 |
105 | ```md
106 | 
107 | ```
108 |
109 | #### All inbuilt themes :-
110 |
111 | dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula
112 |
113 |
114 |
115 | 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
116 |
117 | ### Customization
118 |
119 | You can customize the appearance of your `Stats Card` or `Repo Card` however you want with URL params.
120 |
121 | Customization Options:
122 |
123 | | Option | type | description | Stats Card (default) | Repo Card (default) | Top Lang Card (default) |
124 | | ------------- | --------- | ------------------------------------------- | -------------------- | ------------------- | ----------------------- |
125 | | title_color | hex color | title color | 2f80ed | 2f80ed | 2f80ed |
126 | | text_color | hex color | body color | 333 | 333 | 333 |
127 | | icon_color | hex color | icon color | 4c71f2 | 586069 | 586069 |
128 | | bg_color | hex color | card bg color | FFFEFE | FFFEFE | FFFEFE |
129 | | line_height | number | control the line-height between text | 30 | N/A | N/A |
130 | | hide | CSV | hides the items specified | undefined | N/A | undefined |
131 | | hide_rank | boolean | hides the ranking | false | N/A | N/A |
132 | | hide_title | boolean | hides the stats title | false | N/A | false |
133 | | hide_border | boolean | hides the stats card border | false | N/A | N/A |
134 | | show_owner | boolean | shows owner name in repo card | N/A | false | N/A |
135 | | show_icons | boolean | shows icons | false | N/A | N/A |
136 | | theme | string | sets inbuilt theme | 'default' | 'default_repocard' | 'default' |
137 | | cache_seconds | number | manually set custom cache control | 1800 | 1800 | 1800 |
138 | | count_private | boolean | counts private contributions too if enabled | false | N/A | N/A |
139 | | layout | string | choose a layout option | N/A | N/A | 'default' |
140 |
141 | > 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
142 |
143 | # GitHub Extra Pins
144 |
145 | GitHub extra pins allow you to pin more than 6 repositories in your profile using a GitHub readme profile.
146 |
147 | Yey! You are no longer limited to 6 pinned repositories.
148 |
149 | ### Usage
150 |
151 | Copy-paste this code into your readme and change the links.
152 |
153 | Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`
154 |
155 | ```md
156 | [](https://github.com/anuraghazra/github-readme-stats)
157 | ```
158 |
159 | ### Demo
160 |
161 | [](https://github.com/anuraghazra/github-readme-stats)
162 |
163 | Use [show_owner](#customization) variable to include the repo's owner username
164 |
165 | [](https://github.com/anuraghazra/github-readme-stats)
166 |
167 | # Top Languages Card
168 |
169 | Top languages card shows github user's top langauges which has been mostly used.
170 |
171 | _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_
172 |
173 | ### Usage
174 |
175 | Copy-paste this code into your readme and change the links.
176 |
177 | Endpoint: `api/top-langs?username=anuraghazra`
178 |
179 | ```md
180 | [](https://github.com/anuraghazra/github-readme-stats)
181 | ```
182 |
183 | ### Hide individual languages
184 |
185 | You can use `?hide=language1,language2` parameter to hide individual languages.
186 |
187 | ```md
188 | [](https://github.com/anuraghazra/github-readme-stats)
189 | ```
190 |
191 | > :warning: **Important:**
192 | > Language names should be uri-escaped, as specified in [Percent Encoding](https://en.wikipedia.org/wiki/Percent-encoding)
193 | > (i.e: `c++` should become `c%2B%2B`, `jupyter notebook` should become `jupyter%20notebook`, etc.)
194 |
195 | ### Compact Language Card Layout
196 |
197 | You can use the `&layout=compact` option to change the card design.
198 |
199 | ```md
200 | [](https://github.com/anuraghazra/github-readme-stats)
201 | ```
202 |
203 | ### Demo
204 |
205 | [](https://github.com/anuraghazra/github-readme-stats)
206 |
207 | - Compact layout
208 |
209 | [](https://github.com/anuraghazra/github-readme-stats)
210 |
211 | ---
212 |
213 | ### All Demos
214 |
215 | - Default
216 |
217 | 
218 |
219 | - Hiding specific stats
220 |
221 | 
222 |
223 | - Showing icons
224 |
225 | 
226 |
227 | - Themes
228 |
229 | Choose from any of the [default themes](#themes)
230 |
231 | 
232 |
233 | - Customizing stats card
234 |
235 | 
236 |
237 | - Customizing repo card
238 |
239 | 
240 |
241 | - Top languages
242 |
243 | [](https://github.com/anuraghazra/github-readme-stats)
244 |
245 | ---
246 |
247 | ### Quick Tip (Align The Repo Cards)
248 |
249 | You usually won't be able to layout the images side by side. To do that you can use this approach:
250 |
251 | ```md
252 |
253 |
254 |
255 |
256 |
257 |
258 | ```
259 |
260 | ## Deploy on your own Vercel instance
261 |
262 | 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!
263 |
264 | 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
265 |
266 | [](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats)
267 |
268 |
269 | Guide on setting up Vercel
270 |
271 | 1. Go to [vercel.com](https://vercel.com/)
272 | 1. Click on `Log in`
273 | 
274 | 1. Sign in with GitHub by pressing `Continue with GitHub`
275 | 
276 | 1. Sign into GitHub and allow access to all repositories, if prompted
277 | 1. Fork this repo
278 | 1. Go back to your [Vercel dashboard](https://vercel.com/dashboard)
279 | 1. Select `Import Project`
280 | 
281 | 1. Select `Import Git Repository`
282 | 
283 | 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)
284 | 
285 | 1. Click deploy, and you're good to go. See your domains to use the API!
286 |
287 |
288 |
289 | ## :sparkling_heart: Support the project
290 |
291 | I open-source almost everything I can, and I try to reply to everyone needing help using these projects. Obviously,
292 | this takes time. You can use this service for free.
293 |
294 | 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 :-
295 |
296 | - Giving proper credit when you use github-readme-stats on your readme, linking back to it :D
297 | - Starring and sharing the project :rocket:
298 | - [](https://www.paypal.me/anuraghazra) - You can make one-time donations via PayPal. I'll probably buy a ~~coffee~~ tea. :tea:
299 |
300 | Thanks! :heart:
301 |
302 | ---
303 |
304 | Contributions are welcomed! <3
305 |
306 | Made with :heart: and JavaScript.
307 |
--------------------------------------------------------------------------------
/readme_cn.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
GitHub Readme Stats
4 | 在你的 README 中获取动态生成的 GitHub 统计信息!
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | 查看 Demo
32 | ·
33 | 报告 bug
34 | ·
35 | 请求增加功能
36 |
37 |
38 | English
39 | ·
40 | Español
41 | ·
42 | 日本語
43 |
44 |
45 | 喜欢这个项目?请考虑捐赠 来帮助它完善!
46 |
47 | # 特性
48 |
49 | - [GitHub 统计卡片](#GitHub-统计卡片)
50 | - [GitHub 更多置顶](#GitHub-更多置顶)
51 | - [热门语言卡片](#热门语言卡片)
52 | - [主题](#主题)
53 | - [自定义](#自定义)
54 | - [自己部署](#自己部署)
55 |
56 | # GitHub 统计卡片
57 |
58 | 将这行代码复制到你的 markdown 文件中,简单如此!
59 |
60 | 更改 `?username=` 的值为你的 GitHub 用户名。
61 |
62 | ```md
63 | [](https://github.com/anuraghazra/github-readme-stats)
64 | ```
65 |
66 | _注: 排名基于用户的统计信息计算得出,详见 [src/calculateRank.js](./src/calculateRank.js)_
67 |
68 | ### 隐藏个别统计项
69 |
70 | 想要隐藏指定统计信息,你可以调用参数 `?hide=`,其值用 `,` 分隔。
71 |
72 | > 选项:`&hide=stars,commits,prs,issues,contribs`
73 |
74 | ```md
75 | 
76 | ```
77 |
78 | ### 把私人贡献计数添加到总提交计数中
79 |
80 | 你可以用参数 `?count_private=true` 把私人贡献计数添加到总提交计数中。
81 |
82 | _注:如果你是自己部署本项目,私人贡献将会默认被计数,如果不是自己部署,你需要分享你的私人贡献计数。_
83 |
84 | > Options: `&count_private=true`
85 |
86 | ```md
87 | 
88 | ```
89 |
90 | ### 显示图标
91 |
92 | 想要显示图标,你可以调用 `show_icons=true` 参数,如下:
93 |
94 | ```md
95 | 
96 | ```
97 |
98 | ### 主题
99 |
100 | 你可以通过现有的主题进行卡片个性化,省去[手动自定义](#自定义)的麻烦。
101 |
102 | 调用 `?theme=THEME_NAME` 参数,如下:
103 |
104 | ```md
105 | 
106 | ```
107 |
108 | #### 所有现有主题
109 |
110 | dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula
111 |
112 |
113 |
114 | 在 [theme config 文件](./themes/index.js) 中查看更多主题,或者 **贡献新的主题** :D
115 |
116 | ### 自定义
117 |
118 | 你可以通过使用 URL 参数的方式,为你的 `Stats Card` 或 `Repo Card` 自定义样式。
119 |
120 | 自定义选项:
121 |
122 | | Option | type | description | Stats Card (default) | Repo Card (default) | Top Lang Card (default) |
123 | | ------------- | --------- | ---------------------------- | -------------------- | ------------------- | ----------------------- |
124 | | title_color | hex color | 标题颜色 | 2f80ed | 2f80ed | 2f80ed |
125 | | text_color | hex color | 字体颜色 | 333 | 333 | 333 |
126 | | icon_color | hex color | 图标颜色 | 4c71f2 | 586069 | 586069 |
127 | | bg_color | hex color | 卡片背景颜色 | FFFEFE | FFFEFE | FFFEFE |
128 | | line_height | number | 文字行高 | 30 | N/A | N/A |
129 | | hide | CSV | 隐藏指定统计项 | undfined | N/A | undefined |
130 | | hide_rank | boolean | 隐藏评分等级 | false | N/A | N/A |
131 | | hide_title | boolean | 隐藏卡片标题 | false | N/A | false |
132 | | hide_border | boolean | 隐藏卡片边框 | false | N/A | N/A |
133 | | show_owner | boolean | 显示 Repo 卡片所属账户用户名 | N/A | false | N/A |
134 | | show_icons | boolean | 显示图标 | false | N/A | N/A |
135 | | theme | string | 设置主题 | 'default' | 'default_repocard' | 'default' |
136 | | cache_seconds | number | 手动设置自定义缓存控制 | 1800 | 1800 | 1800 |
137 | | count_private | boolean | 统计私人贡献计数 | false | N/A | N/A |
138 | | layout | string | 布局方式 | N/A | N/A | 'default' |
139 |
140 | > 注意缓存:Repo 卡片默认缓存 30 分钟,如果 fork 数和 star 数小于 1k ,则默认为 2 小时。缓存被限制为最少 30 分钟,最长 24 小时。
141 |
142 | # GitHub 更多置顶
143 |
144 | GitHub 更多置顶 让你使用 README Profile,在个人页面中置顶多于 6 个 repo 。
145 |
146 | 这波可以!你再也不用受限于最多 6 个置顶了。
147 |
148 | ### 使用细则
149 |
150 | 复制粘贴这段代码到你的 README 文件中,并更改链接。
151 |
152 | Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`
153 |
154 | ```md
155 | [](https://github.com/anuraghazra/github-readme-stats)
156 | ```
157 |
158 | ### Demo
159 |
160 | [](https://github.com/anuraghazra/github-readme-stats)
161 |
162 | 使用 [show_owner](#自定义) 变量将 Repo 所属账户的用户名包含在内。
163 |
164 | [](https://github.com/anuraghazra/github-readme-stats)
165 |
166 | # 热门语言卡片
167 |
168 | 热门语言卡片显示了 GitHub 用户常用的编程语言。
169 |
170 | _注意:热门语言并不表示我的技能水平或类似的水平,它是用户在 GitHub 上拥有最多代码的一项指标,它是 github-readme-stats 的新功能_
171 |
172 | ### 使用细则
173 |
174 | 将此代码复制粘贴到您的`README.md`文件中,并改变链接。
175 |
176 | Endpoint: `api/top-langs?username=anuraghazra`
177 |
178 | ```md
179 | [](https://github.com/anuraghazra/github-readme-stats)
180 | ```
181 |
182 | ### 隐藏特定语言
183 |
184 | 可以使用`?hide=语言1,语言2`参数来隐藏指定的语言。
185 |
186 | ```md
187 | [](https://github.com/anuraghazra/github-readme-stats)
188 | ```
189 |
190 | ### 紧凑的语言卡片布局
191 |
192 | 你可以使用 `&layout=compact` 参数来改变卡片的样式。
193 |
194 | ```md
195 | [](https://github.com/anuraghazra/github-readme-stats)
196 | ```
197 |
198 | ### Demo
199 |
200 | [](https://github.com/anuraghazra/github-readme-stats)
201 |
202 | - 紧凑布局
203 |
204 | [](https://github.com/anuraghazra/github-readme-stats)
205 |
206 | ---
207 |
208 | ### 全部 Demo
209 |
210 | - 默认
211 |
212 | 
213 |
214 | - 隐藏特定数据
215 |
216 | 
217 |
218 | - 显示图标
219 |
220 | 
221 |
222 | - 主题
223 |
224 | 从 [默认主题](#主题) 中进行选择
225 |
226 | 
227 |
228 | - 自定义统计卡片
229 |
230 | 
231 |
232 | - 自定义代码库卡片
233 |
234 | 
235 |
236 | - 热门语言
237 |
238 | [](https://github.com/anuraghazra/github-readme-stats)
239 |
240 | ---
241 |
242 | ### 提示 (对齐 Repo 卡片)
243 |
244 | 你通常无法将图片靠边显示。为此,您可以使用以下方法:
245 |
246 | ```md
247 |
248 |
249 |
250 |
251 |
252 |
253 | ```
254 |
255 | ## 自己部署
256 |
257 | 因为 GitHub 的 API 每个小时只允许 5 千次请求,我的 `https://github-readme-stats.vercel.app/api` 很有可能会触发限制
258 | 如果你将其托管在自己的 Vercel 服务器上,那么你就不必为此担心。点击 deploy 按钮来开始你的部署!
259 |
260 | 注意: 从 [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) 开始,我们应该能够处理超过 5 千 的请求,并且不会出现宕机问题 :D
261 |
262 | [](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats)
263 |
264 |
265 | 设置 Vercel 的指导
266 |
267 | 1. 前往 [vercel.com](https://vercel.com/)
268 | 1. 点击 `Log in`
269 | 
270 | 1. 点击 `Continue with GitHub` 通过 GitHub 进行登录
271 | 
272 | 1. 登录 GitHub 并允许访问所有存储库(如果系统这样提示)
273 | 1. Fork 这个仓库
274 | 1. 返回到你的 [Vercel dashboard](https://vercel.com/dashboard)
275 | 1. 选择 `Import Project`
276 | 
277 | 1. 选择 `Import Git Repository`
278 | 
279 | 1. 选择 root 并将所有内容保持不变,并且只需添加名为 PAT_1 的环境变量(如图所示),其中将包含一个个人访问令牌(PAT),你可以在[这里](https://github.com/settings/tokens/new)轻松创建(保留默认,并且只需要命名下,名字随便)
280 | 
281 | 1. 点击 deploy,这就完成了,查看你的域名就可使用 API 了!
282 |
283 |
284 |
285 | ## :sparkling_heart: 支持这个项目
286 |
287 | 我尽己所能地进行开源,并且我尽量回复每个在使用项目时需要帮助的人。很明显,这需要时间,但你可以免费享受这些。
288 |
289 | 然而, 如果你正在使用这个项目并感觉良好,或只是想要支持我继续开发,你可以通过如下方式:
290 |
291 | - 在你的 README 中使用 github-readme-stats 时,链接指向会这里 :D
292 | - Star 并 分享这个项目 :rocket:
293 | - [](https://www.paypal.me/anuraghazra) - 你可以通过 PayPal 一次性捐款. 我多半会买一杯 ~~咖啡~~ 茶。:tea:
294 |
295 | 谢谢! :heart:
296 |
297 | ---
298 |
299 | 欢迎贡献! <3
300 |
301 | 用 :heart: 发电,用 JavaScript 制作。
302 |
--------------------------------------------------------------------------------
/readme_de.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
GitHub Readme Stats
4 | Zeige dynamisch generierte GitHub-Statistiken in deinen Readmes!
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | Beispiel ansehen
32 | ·
33 | Fehler melden
34 | ·
35 | Funktionalität anfragen
36 |
37 |
38 | Du magst das Projekt? Wie wäre es mit einer kleinen Spende um es weiterhin am Leben zu erhalten?
39 |
40 | # Funktionalitäten
41 |
42 | - [GitHub Stats Card](#github-stats-card)
43 | - [GitHub Extra Pins](#github-extra-pins)
44 | - [Top Programmiersprachen Card](#top-programmiersprachen-card)
45 | - [Erscheinungsbild/Themes](#erscheinungsbildthemes)
46 | - [Anpassungen](#anpassungenpersonalisierung)
47 | - [Selber betreiben](#betreibe-es-auf-deiner-eigenen-vercel-instanz)
48 |
49 | # GitHub Stats Card
50 |
51 | Kopiere einfach folgendes in dein Markdown und das wars. Echt simpel!
52 |
53 | Passe den Wert des URL-Paramters `?username=` so an, dass dort dein GitHub Username steht.
54 |
55 | ```md
56 | [](https://github.com/anuraghazra/github-readme-stats)
57 | ```
58 |
59 | _Hinweis: Die Berechnung des Ranges basiert auf den jeweiligen Benutzerstatistiken, siehe [src/calculateRank.js](./src/calculateRank.js)_
60 |
61 | ### Verbergen individueller Statistiken
62 |
63 | Um eine spezifische Statistik auszublenden, kann dem Query-Parameter `?hide=` ein Array an Dingen die nicht angezeigt werden sollen übergeben werden.
64 |
65 | > Optionen: `&hide=["stars","commits","prs","issues","contribs"]`
66 |
67 | ```md
68 | 
69 | ```
70 |
71 | ### Icons anzeigen
72 |
73 | Um Icons anzuzeigen kann der URL-Paramter `show_icons=true` wie folgt verwendet werden:
74 |
75 | ```md
76 | 
77 | ```
78 |
79 | ### Erscheinungsbild/Themes
80 |
81 | Mithilfe der eingebauten Themes kann das Aussehen der Karten verändern werden ohne manuelle Anpassungen vornehmen zu müssen.
82 |
83 | Benutze den `?theme=THEME_NAME`-Parameter wie folgt :-
84 |
85 | ```md
86 | 
87 | ```
88 |
89 | #### Alle eingebauten Themes :-
90 |
91 | dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula
92 |
93 |
94 |
95 | Du kannst dir eine Vorschau [aller verfügbaren Themes](./themes/README.md) ansehen oder die [theme config Datei](./themes/index.js) auschecken.
96 | Außerdem **kannst du neue Themes beisteuern** wenn du möchtest, contributions sind gern gesehen :D
97 |
98 | ### Anpassungen/Personalisierung
99 |
100 | Du kannst das Erscheinungsbild deiner `Stats Card` oder `Repo Card`, mithilfe von URL-Parametern, nach deinen Vorlieben anpassen.
101 |
102 | Anpassungsoptionen:
103 |
104 | | Option | Type | Beschreibung | Statistiken (default) | Repo (default) | Top Programmiersprachen (default) |
105 | | ---------------- | --------- | ---------------------------------------------------- | --------------------- | ------------------ | --------------------------------- |
106 | | title_color | hex color | Titelfarbe | 2f80ed | 2f80ed | 2f80ed |
107 | | text_color | hex color | Textkörperfarbe | 333 | 333 | 333 |
108 | | icon_color | hex color | Iconfarbe | 4c71f2 | 586069 | 586069 |
109 | | bg_color | hex color | Hintergrundfarbe | FFFEFE | FFFEFE | FFFEFE |
110 | | line_height | number | kontrolliert die Zeilenhöhe zwischen dem Text | 30 | N/A | N/A |
111 | | hide_rank | boolean | blendet das Ranking aus | false | N/A | N/A |
112 | | hide_title | boolean | blendet den Statistik-Titel aus | false | N/A | false |
113 | | hide_border | boolean | blendet den Rahmen aus | false | N/A | N/A |
114 | | show_owner | boolean | zeigt den Namen des Besitzers in der Repo-Karte | N/A | false | N/A |
115 | | show_icons | boolean | zeige Icons an | false | N/A | N/A |
116 | | theme | string | setze eingebaute themes | 'default' | 'default_repocard' | 'default' |
117 | | cache_seconds | number | manuelles setzen der Cachezeiten | 1800 | 1800 | 1800 |
118 | | hide_langs_below | number | verberge Sprachen unter einem bestimmten Schwellwert | N/A | N/A | undefined |
119 |
120 | > Hinweis bzgl. des Caches: Wenn die Anzahl der Forks und Stars geringer als 1Tsd ist, haben die Repo-Cards eine default Cachezeit von 30 Minuten (1800 Sekunden), ansonsten beträgt diese 2 Stunden (7200 Sekunden). Außerdem ist der Cache auf eine Minimum von 30 Minuten und ein Maximum von 24 Stunden begrenzt.
121 |
122 | # GitHub Extra Pins
123 |
124 | GitHub extra pins ermöglicht es, mit Hilfe eines GitHub-Readme-Profiles, mehr als 6 Repositories in deinen Profil anzuzeigen.
125 |
126 | Und Bääm! Du bist nicht mehr auf 6 pinned Repositories limitiert.
127 |
128 | ### Benutzung
129 |
130 | Füge diesen Code in deine Readme-Datei ein und passe die Links an.
131 |
132 | Endpunkt: `api/pin?username=anuraghazra&repo=github-readme-stats`
133 |
134 | ```md
135 | [](https://github.com/anuraghazra/github-readme-stats)
136 | ```
137 |
138 | ### Beispiele
139 |
140 | [](https://github.com/anuraghazra/github-readme-stats)
141 |
142 | Benutze die [show_owner](#anpassungenpersonalisierung) Variable, um den Usernamen des Repo Eigentümers anzuzeigen.
143 |
144 | [](https://github.com/anuraghazra/github-readme-stats)
145 |
146 | # Top Programmiersprachen Card
147 |
148 | Die Top Programmiersprachen Card visualisiert die am meisten benutzten Programmiersprachen eines GitHub-Nutzers.
149 |
150 | _HINWEIS: Die Top Programmiersprachen treffen keine Aussage über persönliche Fähigkeiten oder der gleichen, es ist lediglich eine auf den GitHub-Statistiken des Nutzers basierende Kennzahl welche Programmiersprache wie häufig verwendet wurde._
151 |
152 | ### Benutzung
153 |
154 | Füge diesen Code in deine Readme-Datei ein und passe die Links an.
155 |
156 | Endpunkt: `api/top-langs?username=anuraghazra`
157 |
158 | ```md
159 | [](https://github.com/anuraghazra/github-readme-stats)
160 | ```
161 |
162 | ### Verberge Programmiersprachen unter einem bestimmten Schwellwert
163 |
164 | Benutze den `?hide_langs_below=NUMBER` URL-Parameter um Programmiersprachen unter einem bestimmten prozentualen Schwellwert auszublenden.
165 |
166 | ```md
167 | [](https://github.com/anuraghazra/github-readme-stats)
168 | ```
169 |
170 | ### Beispiel
171 |
172 | [](https://github.com/anuraghazra/github-readme-stats)
173 |
174 | ---
175 |
176 | ### Alle Beispiele
177 |
178 | - Default
179 |
180 | 
181 |
182 | - Ausblenden bestimmter Statistiken
183 |
184 | 
185 |
186 | - Icons anzeigen
187 |
188 | 
189 |
190 | - Erscheinungsbild/Themes
191 |
192 | Choose from any of the [default themes](#themes)
193 |
194 | 
195 |
196 | - Stats Card anpassen
197 |
198 | 
199 |
200 | - Repo Card anpassen
201 |
202 | 
203 |
204 | - Top Programmiersprachen
205 |
206 | [](https://github.com/anuraghazra/github-readme-stats)
207 |
208 | ---
209 |
210 | ### Kleiner Tip (Ausrichten der Repo Cards)
211 |
212 | Üblicherweise ist es in `.md`-Dateien nicht möglich Bilder nebeneinander anzuzeigen. Um dies zu erreichen kann folgender Ansatz gewählt werden:
213 |
214 | ```md
215 |
216 |
217 |
218 |
219 |
220 |
221 | ```
222 |
223 | ## Betreibe es auf deiner eigenen Vercel-Instanz
224 |
225 | Da die GitHub API nur 5tsd Aufrufe pro Stunde zulässt, kann es passieren, dass meine `https://github-readme-stats.vercel.app/api` dieses Limit erreicht.
226 | Wenn du es auf deinem eigenen Vercel-Server hostest, brauchst du dich darum nicht zu kümmern. Klicke auf den Deploy-Button um loszulegen!
227 |
228 | Hinweis: Seit [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) sollte es möglich sein mehr als 5tsd Aufrufe pro Stunde ohne Downtimes zu verkraften :D
229 |
230 | [](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats)
231 |
232 |
233 | Anleitung zum Einrichten von Vercel
234 |
235 | 1. Gehe zu [vercel.com](https://vercel.com/)
236 | 1. Klicke auf `Log in`
237 | 
238 | 1. Melde dich mit deinem GitHub-account an, indem du `Continue with GitHub` klickst
239 | 
240 | 1. Verbinde dich mit GitHub und erlaube den Zugriff auf alle Repositories, wenn gefordert
241 | 1. Forke dieses Repository
242 | 1. Gehe zurück zu deinem [Vercel dashboard](https://vercel.com/dashboard)
243 | 1. Klick `Import Project`
244 | 
245 | 1. Klick `Import Git Repository`
246 | 
247 | 1. Wähle root und füge eine Umgebungsvariable namens PAT_1 (siehe Abbildung) die als Wert deinen persönlichen Access Token (PAT) hat hinzu, den du einfach [hier](https://github.com/settings/tokens/new) erzeugen kannst (lasse alles wie es ist, vergebe einen beliebigen Namen)
248 | 
249 | 1. Klicke deploy, und das wars. Besuche deine domains um die API zu benutzen!
250 |
251 |
252 | ## :sparkling_heart: Unterstütze das Projekt
253 |
254 | Ich versuche alles was ich kann als Open-Source zur Verfügung zu stellen, als auch jedem der Hilfe bei der Benutzung dieses Projektes braucht zu antworten. Natürlich beansprucht sowas Zeit und Du kannst diesen Dienst kostenlos benutzen.
255 |
256 | Dennoch, wenn Du dieses Projekt benutzt und damit zufrieden bist oder mich einfach nur motivieren möchtest weiterhin daran zu arbeiten, gibt es verschiedene Sachen die Du machen kannst:-
257 |
258 | - Erwähne und verlinke das Projekt in deiner Readme wenn du es benutzt :D
259 | - Geb dem Projekt einen Stern hier auf GitHub und teile es :rocket:
260 | - [](https://www.paypal.me/anuraghazra) - Du kannst einmalige Spenden via PayPal tätigen. Ich kaufe mir wahrscheinlich einen ~~Kaffee~~ Tee davon. :tea:
261 |
262 | Vielen Dank! :heart:
263 |
264 | ---
265 |
266 | Mitarbeit an dem Projekt is immer Willkommen! <3
267 |
268 | Gebaut mit :heart: und JavaScript.
269 |
--------------------------------------------------------------------------------
/readme_es.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
GitHub Readme Stats
4 | ¡Obtén tus estadísticas de GitHub generadas dinámicamente en tu README!
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | Ve un ejemplo
29 | ·
30 | Reporta un bug
31 | ·
32 | Solicita una mejora
33 |
34 |
35 | English
36 | ·
37 | 简体中文
38 | ·
39 | 日本語
40 |
41 |
42 | ¿Te gusta este proyecto? ¡Por favor considera donar para ayudar a mejorarlo!
43 |
44 | # Características
45 |
46 | - [Tarjeta de estadísticas de GitHub](#tarjeta-de-estadísticas-de-github)
47 | - [Pins extra de GitHub](#pins-extra-de-github)
48 | - [Temas](#temas)
49 | - [Personalización](#personalización)
50 | - [Despliega por tu cuenta](#despliega-tu-propia-instancia-de-vercel)
51 |
52 | # Tarjeta de estadísticas de GitHub
53 |
54 | Copia y pega esto en el contenido de tu README.md y listo. ¡Simple!
55 |
56 | Cambia el valor `?username=` al nombre de tu usuario de GitHub.
57 |
58 | ```md
59 | [](https://github.com/anuraghazra/github-readme-stats)
60 | ```
61 |
62 | _Nota: las clasificaciones se calculan basándose en las estadísticas del usuario. Ve [src/calculateRank.js](./src/calculateRank.js)._
63 |
64 | ### Ocultar estadísticas individualmente
65 |
66 | Para ocultar alguna estadística específica, puedes utilizar el parámetro `?hide=` con un arreglo de items que quieras ocultar.
67 |
68 | > Opciones: `&hide=["stars","commits","prs","issues","contribs"]`
69 |
70 | ```md
71 | 
72 | ```
73 |
74 | ### Agregar contribuciones privadas al total de commits contados
75 |
76 | Puede agregar el recuento de todas sus contribuciones privadas al recuento total de confirmaciones utilizando el parámetro de consulta `?count_private=true`.
77 |
78 | _Nota: Si está desplegando este proyecto usted mismo, las contribuciones privadas se contarán de manera predeterminada; de lo contrario, deberá elegir compartir sus recuentos de contribuciones privadas._
79 |
80 | > Opciones: `&count_private=true`
81 |
82 | ```md
83 | 
84 | ```
85 |
86 | ### Mostrar íconos
87 |
88 | Para habilitar los íconos, puedes utilizar `show_icons=true` como parámetro, de esta manera:
89 |
90 | ```md
91 | 
92 | ```
93 |
94 | ### Temas
95 |
96 | Puedes personalizar el aspecto de la tarjeta sin realizar ninguna [personalización manual](#personalización) con los temas incorporados.
97 |
98 | Utiliza el parámetro `?theme=THEME_NAME`, de esta manera:
99 |
100 | ```md
101 | 
102 | ```
103 |
104 | #### Todos los temas incorporados
105 |
106 | dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula
107 |
108 |
109 |
110 | 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
111 |
112 | ### Personalización
113 |
114 | Puedes personalizar el aspecto de tu `Stats Card` o `Repo Card` de la manera que desees con los parámetros URL.
115 |
116 | Opciones de personalización:
117 |
118 | | Option | type | description | Stats Card (default) | Repo Card (default) |
119 | | ----------- | --------- | ----------------------------- | -------------------- | ------------------- |
120 | | title_color | hex color | color del título | 2f80ed | 2f80ed |
121 | | text_color | hex color | color del contenido | 333 | 333 |
122 | | icon_color | hex color | color del ícono | 4c71f2 | 586069 |
123 | | bg_color | hex color | color de fondo | FFFEFE | FFFEFE |
124 | | line_height | number | controla el line_height | 30 | N/A |
125 | | hide_rank | boolean | oculta la clasificación | false | N/A |
126 | | hide_title | boolean | oculta el título | false | N/A |
127 | | hide_border | boolean | oculta el borde | false | N/A |
128 | | show_owner | boolean | muestra el propietario | N/A | false |
129 | | show_icons | boolean | muestra los íconos | false | N/A |
130 | | theme | string | establece un tema incorporado | 'default' | 'default_repocard' |
131 |
132 | ---
133 |
134 | ### Ejemplo
135 |
136 | - Predeterminado
137 |
138 | 
139 |
140 | - Ocultando estadísticas específicas
141 |
142 | 
143 |
144 | - Mostrando íconos
145 |
146 | 
147 |
148 | - Temas
149 |
150 | Elige uno de los [temas predeterminados](#temas)
151 |
152 | 
153 |
154 | - Personalizando la tarjeta de estadísticas
155 |
156 | 
157 |
158 | - Personalizando la tarjeta de repositorio
159 |
160 | 
161 |
162 | ---
163 |
164 | # Pins extra de GitHub
165 |
166 | Los pins extra de GitHub te permiten anclar más de 6 repositorios en tu perfil utilizando el archivo README.md.
167 |
168 | ¡Bien! Ya no estás limitado a 6 repositorios anclados.
169 |
170 | ### Utilización
171 |
172 | Copia y pega este código en tu README.md y cambia los links.
173 |
174 | Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`
175 |
176 | ```md
177 | [](https://github.com/anuraghazra/github-readme-stats)
178 | ```
179 |
180 | ### Ejemplo
181 |
182 | [](https://github.com/anuraghazra/github-readme-stats)
183 |
184 | Utiliza la variable [show_owner](#customización) para incluir el nombre de usuario del propietario del repositorio.
185 |
186 | [](https://github.com/anuraghazra/github-readme-stats)
187 |
188 | ### Pequeño consejo (alinear las tarjetas de repositorios)
189 |
190 | Usualmente no serías capaz de alinear las imágenes una al lado de otra. Para lograrlo, puedes realizar esto:
191 |
192 | ```md
193 |
194 |
195 |
196 |
197 |
198 |
199 | ```
200 |
201 | ## Despliega tu propia instancia de vercel
202 |
203 | 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!
204 |
205 | 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
206 |
207 | [](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats)
208 |
209 |
210 | Guía para comenzar en Vercel
211 |
212 | 1. Ve a [vercel.com](https://vercel.com/)
213 | 1. Clickea en `Log in`
214 | 
215 | 1. Inicia sesión con GitHub presionando `Continue with GitHub`
216 | 
217 | 1. Permite el acceso a todos los repositorios (si se te pregunta)
218 | 1. Haz un Fork de este repositorio
219 | 1. Dirígete de nuevo a tu [Vercel dashboard](https://vercel.com/dashboard)
220 | 1. Selecciona `Import Project`
221 | 
222 | 1. Selecciona `Import Git Repository`
223 | 
224 | 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)
225 | 
226 | 1. Clickea "Deploy" y ya está listo. ¡Ve tus dominios para usar la API!
227 |
228 |
229 |
230 | ## :sparkling_heart: Apoya al proyecto
231 |
232 | Casi todos mis proyectos son código-abierto e intento responder a todos los usuarios que necesiten ayuda con alguno de estos proyectos, Obviamente,
233 | esto toma tiempo. Puedes usar este servicio gratis.
234 |
235 | 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:
236 |
237 | - Darme créditos cuando estés utilizando github-readme-stats en tu README, añadiendo un link a este repositorio :D
238 | - Dándole una estrella (starring) y compartiendo el proyecto :rocket:
239 | - [](https://www.paypal.me/anuraghazra) - Puedes hacerme una única donación a través de PayPal. Probablemente me compraré un ~~café~~ té. :tea:
240 |
241 | ¡Gracias! :heart:
242 |
243 | ---
244 |
245 | ¡Las contribuciones son bienvenidas! <3
246 |
247 | Hecho con :heart: y JavaScript.
248 |
--------------------------------------------------------------------------------
/readme_ja.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
GitHub Readme Stats
4 | あなたのREADMEに動的に生成されたGitHubの統計情報を載せましょう!
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | View Demo
32 | ·
33 | Report Bug
34 | ·
35 | Request Feature
36 |
37 |
38 | English
39 | ·
40 | 简体中文
41 | ·
42 | Español
43 |
44 |
45 | このプロジェクトを気に入っていただけましたか? もしよろしければ、プロジェクトのさらなる改善のために寄付 を検討して頂けると嬉しいです!
46 |
47 | # Features
48 |
49 | - [GitHub Stats Card](#github-stats-card)
50 | - [GitHub Extra Pins](#github-extra-pins)
51 | - [Top Languages Card](#top-languages-card)
52 | - [Themes](#themes)
53 | - [Customization](#customization)
54 | - [Deploy Yourself](#deploy-on-your-own-vercel-instance)
55 |
56 | # GitHub Stats Card
57 |
58 | 以下の構文をコピーして、あなたの Markdown ファイルに貼り付けるだけです。
59 | 簡単ですね!
60 |
61 | `?username=` の値は、あなたの GitHub アカウントのユーザー名に変更してください。
62 |
63 | ```md
64 | [](https://github.com/anuraghazra/github-readme-stats)
65 | ```
66 |
67 | _Note: カードに表示されるランクはユーザの統計情報に基づいて計算されています。詳しくは、[src/calculateRank.js](./src/calculateRank.js)を見てください。_
68 |
69 | ### Hiding individual stats
70 |
71 | クエリパラメータ `?hide=` にカンマ区切りの値を渡すことで、特定の統計情報を隠すことができます。
72 |
73 | > Options: `&hide=stars,commits,prs,issues,contribs`
74 |
75 | ```md
76 | 
77 | ```
78 |
79 | ### Adding private contributions count to total commits count
80 |
81 | クエリパラメータ `?count_private=true` を使用することで、private contributions の数をコミット総数に追加することができます。
82 |
83 | _Note: このプロジェクトを自分でデプロイしている場合、デフォルトでは非公開の貢献がカウントされます。_
84 |
85 | > Options: `&count_private=true`
86 |
87 | ```md
88 | 
89 | ```
90 |
91 | ### Showing icons
92 |
93 | クエリパラメータ `?show_icons=true` を使用することで、アイコンが表示が有効になります。
94 |
95 | ```md
96 | 
97 | ```
98 |
99 | ### Themes
100 |
101 | 内蔵されているテーマを使用することで、任意の[手動のカスタマイズ](#customization)を行うことなく、カードの外観をカスタマイズすることができます。
102 |
103 | `?theme=THEME_NAME` は以下のように使います。
104 |
105 | ```md
106 | 
107 | ```
108 |
109 | #### All inbuilt themes :-
110 |
111 | dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula
112 |
113 |
114 |
115 | 用意されている全てのテーマの[プレビュー](./themes/README.md)や[設定ファイル](./themes/index.js)を見ることができます。もしよろしければ、**新しいテーマを投稿してみてください** (´∀` )
116 |
117 | ### Customization
118 |
119 | `Stats Card` や `Repo Card` の外観を URL パラメーターを使って好きなようにカスタマイズすることができます。
120 |
121 | Customization Options:
122 |
123 | | Option | type | description | Stats Card (default) | Repo Card (default) | Top Lang Card (default) |
124 | | ------------- | --------- | -------------------------------------------- | -------------------- | ------------------- | ----------------------- |
125 | | title_color | hex color | タイトルの色 | 2f80ed | 2f80ed | 2f80ed |
126 | | text_color | hex color | 文字の色 | 333 | 333 | 333 |
127 | | icon_color | hex color | アイコンの色 | 4c71f2 | 586069 | 586069 |
128 | | bg_color | hex color | カードの背景色 | FFFEFE | FFFEFE | FFFEFE |
129 | | line_height | number | 字間距離 | 30 | N/A | N/A |
130 | | hide | CSV | 項目の非表示 | undefined | N/A | undefined |
131 | | hide_rank | boolean | ranking の非表示 | false | N/A | N/A |
132 | | hide_title | boolean | タイトルの非表示 | false | N/A | false |
133 | | hide_border | boolean | 枠線の非表示 | false | N/A | N/A |
134 | | show_owner | boolean | オーナー名の表示 | N/A | false | N/A |
135 | | show_icons | boolean | アイコンの表示 | false | N/A | N/A |
136 | | theme | string | 用意されているテーマ | 'default' | 'default_repocard' | 'default' |
137 | | cache_seconds | number | キャッシュコントロール | 1800 | 1800 | 1800 |
138 | | count_private | boolean | private contributions 数をコミット総数に追加 | false | N/A | N/A |
139 | | layout | string | レイアウトのオプション選択 | N/A | N/A | 'default' |
140 |
141 | > キャッシュに関する注意点: Repo cards のデフォルトのキャッシュは、フォーク数とスター数が 1k 未満の場合は 30 分(1800 秒) で、それ以外の場合は 2 時間(7200) です。また、キャッシュは最低でも 30 分、最大でも 24 時間に制限されていることに注意してください。
142 |
143 | # GitHub Extra Pins
144 |
145 | GitHub extra pins を使うと、GitHub の readme プロフィールを使って、自分のプロフィールに 6 つ以上のリポジトリをピン留めすることができます。
146 |
147 | イェーイ! もはや 6 つのピン留めされたリポジトリに制限されることはありません。
148 |
149 | ### Usage
150 |
151 | 以下のコードをあなたの readme にコピー & ペーストし、リンクを変更してください。
152 |
153 | Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`
154 |
155 | ```md
156 | [](https://github.com/anuraghazra/github-readme-stats)
157 | ```
158 |
159 | ### Demo
160 |
161 | [](https://github.com/anuraghazra/github-readme-stats)
162 |
163 | リポジトリのオーナーのユーザー名を含める場合は、show_owner 変数を使用します。
164 |
165 | [](https://github.com/anuraghazra/github-readme-stats)
166 |
167 | # Top Languages Card
168 |
169 | Top languages card には、その GitHub ユーザーが最も利用している Top languages が表示されます。
170 |
171 | _NOTE: Top languages は、ユーザのスキルレベルを示すものではなく、GitHub 上でどの言語で最も多くのコードを書いているかを示す GitHub の指標です。_
172 |
173 | ### Usage
174 |
175 | 以下のコードをあなたの readme にコピー & ペーストし、リンクを変更してください。
176 |
177 | Endpoint: `api/top-langs?username=anuraghazra`
178 |
179 | ```md
180 | [](https://github.com/anuraghazra/github-readme-stats)
181 | ```
182 |
183 | ### Hide individual languages
184 |
185 | クエリパラメータ `?hide=language1,language2` 使用することで、個々の言語を非表示にすることができます。
186 |
187 | ```md
188 | [](https://github.com/anuraghazra/github-readme-stats)
189 | ```
190 |
191 | ### Compact Language Card Layout
192 |
193 | クエリパラメータ `&layout=compact` を使用することで、カードのデザインを変更することができます。
194 |
195 | ```md
196 | [](https://github.com/anuraghazra/github-readme-stats)
197 | ```
198 |
199 | ### Demo
200 |
201 | [](https://github.com/anuraghazra/github-readme-stats)
202 |
203 | - Compact layout
204 |
205 | [](https://github.com/anuraghazra/github-readme-stats)
206 |
207 | ---
208 |
209 | ### All Demos
210 |
211 | - Default
212 |
213 | 
214 |
215 | - Hiding specific stats
216 |
217 | 
218 |
219 | - Showing icons
220 |
221 | 
222 |
223 | - Themes
224 |
225 | 任意の[テーマ](#themes)を選択できます。
226 |
227 | 
228 |
229 | - Customizing stats card
230 |
231 | 
232 |
233 | - Customizing repo card
234 |
235 | 
236 |
237 | - Top languages
238 |
239 | [](https://github.com/anuraghazra/github-readme-stats)
240 |
241 | ---
242 |
243 | ### Quick Tip (Align The Repo Cards)
244 |
245 | 通常、画像を並べてレイアウトすることはできません。そのためには、次のような方法があります。
246 |
247 | ```md
248 |
249 |
250 |
251 |
252 |
253 |
254 | ```
255 |
256 | ## Deploy on your own Vercel instance
257 |
258 | GitHub API は 1 時間あたり 5k リクエストしか受け付けていないので、私の `https://github-readme-stats.vercel.app/api` がレートリミッターを超えてしまう可能性があります。自分の Vercel サーバーでホストしているのであれば、何も心配する必要はありません。デプロイボタンをクリックして始めましょう!
259 |
260 | NOTE: [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) 以降は 5k 以上のリクエストに対応できるようになり、ダウンタイムの問題もなくなりました (´∀` )
261 |
262 | [](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats)
263 |
264 |
265 | Vercelの設定ガイド
266 |
267 | 1. [vercel.com](https://vercel.com/)に行きます。
268 | 1. `Log in`をクリックします。
269 | 
270 | 1. `Continue with GitHub` を押して GitHub にサインインします。
271 | 
272 | 1. GitHub にサインインし、すべてのリポジトリへのアクセスを許可します。
273 | 1. このリポジトリをフォークします。
274 | 1. [Vercel dashboard](https://vercel.com/dashboard)に戻ります。
275 | 1. `Import Project` を選択します。
276 | 
277 | 1. `Import Git Repository` を選択します。
278 | 
279 | 1. root を選択して、すべてをそのままにしておき、PAT_1 という名前の環境変数を(下図のように)追加します。これには個人アクセストークン (PAT) が含まれており、[ここ](https://github.com/settings/tokens/new)で簡単に作成することができます (すべてをそのままにしておいて、何かに名前を付けてください。)
280 | 
281 | 1. デプロイをクリックすれば完了です。API を使用するためにあなたのドメインを参照してください!
282 |
283 |
284 |
285 | ## :sparkling_heart: Support the project
286 |
287 | 私はできる限りのことをオープンソースで行い、これらのプロジェクトを利用して支援を必要としている皆さんに返信するようにしています。もちろんです。
288 | これは時間がかかります。無料でご利用いただけます。
289 |
290 | しかし、もしあなたがこのプロジェクトを使っていて、それに満足しているのであれば、あるいは単に私にものを作り続けることを奨励したいのであれば、いくつかの方法があります。
291 |
292 | - あなたの readme で github-readme-stats を使用して適切なクレジットを付与し、それにリンクします (´∀` )
293 | - 主演とプロジェクトの共有 :rocket:
294 | - [](https://www.paypal.me/anuraghazra) - PayPal を介して 1 回限りの寄付を行うことができます。私はおそらく ~~コーヒー~~ お茶買うでしょう。 :tea:
295 |
296 | Thanks! :heart:
297 |
298 | ---
299 |
300 | Contributions を歓迎します! <3
301 |
302 | このプロジェクトは :heart: と JavaScript で作られています。
303 |
--------------------------------------------------------------------------------
/scripts/generate-theme-doc.js:
--------------------------------------------------------------------------------
1 | const theme = require("../themes/index");
2 | const fs = require("fs");
3 |
4 | const TARGET_FILE = "./themes/README.md";
5 | const REPO_CARD_LINKS_FLAG = "";
6 | const STAT_CARD_LINKS_FLAG = "";
7 |
8 | const STAT_CARD_TABLE_FLAG = "";
9 | const REPO_CARD_TABLE_FLAG = "";
10 |
11 | const THEME_TEMPLATE = `## Available Themes
12 |
13 |
14 |
15 | With inbuilt themes you can customize the look of the card without doing any manual customization.
16 |
17 | Use \`?theme=THEME_NAME\` parameter like so :-
18 |
19 | \`\`\`md
20 | 
21 | \`\`\`
22 |
23 | ## Stats
24 |
25 | > These themes work both for the Stats Card and Repo Card.
26 |
27 | | | | |
28 | | :--: | :--: | :--: |
29 | ${STAT_CARD_TABLE_FLAG}
30 |
31 | ## Repo Card
32 |
33 | > These themes work both for the Stats Card and Repo Card.
34 |
35 | | | | |
36 | | :--: | :--: | :--: |
37 | ${REPO_CARD_TABLE_FLAG}
38 |
39 | ${STAT_CARD_LINKS_FLAG}
40 |
41 | ${REPO_CARD_LINKS_FLAG}
42 |
43 |
44 | [add-theme]: https://github.com/anuraghazra/github-readme-stats/edit/master/themes/index.js
45 |
46 | Wanted to add a new theme? Consider reading the [contribution guidelines](../CONTRIBUTING.md#themes-contribution) :D
47 | `;
48 |
49 | const createRepoMdLink = (theme) => {
50 | return `\n[${theme}_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=${theme}`;
51 | };
52 | const createStatMdLink = (theme) => {
53 | return `\n[${theme}]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=${theme}`;
54 | };
55 |
56 | const generateLinks = (fn) => {
57 | return Object.keys(theme)
58 | .map((name) => fn(name))
59 | .join("");
60 | };
61 |
62 | const createTableItem = ({ link, label, isRepoCard }) => {
63 | if (!link || !label) return "";
64 | return `\`${label}\` ![${link}][${link}${isRepoCard ? "_repo" : ""}]`;
65 | };
66 | const generateTable = ({ isRepoCard }) => {
67 | const rows = [];
68 | const themes = Object.keys(theme).filter(
69 | (name) => name !== (!isRepoCard ? "default_repocard" : "default")
70 | );
71 |
72 | for (let i = 0; i < themes.length; i += 3) {
73 | const one = themes[i];
74 | const two = themes[i + 1];
75 | const three = themes[i + 2];
76 |
77 | let tableItem1 = createTableItem({ link: one, label: one, isRepoCard });
78 | let tableItem2 = createTableItem({ link: two, label: two, isRepoCard });
79 | let tableItem3 = createTableItem({ link: three, label: three, isRepoCard });
80 |
81 | if (three === undefined) {
82 | tableItem3 = `[Add your theme][add-theme]`;
83 | }
84 | rows.push(`| ${tableItem1} | ${tableItem2} | ${tableItem3} |`);
85 |
86 | // if it's the last row & the row has no empty space push a new row
87 | if (three && i + 3 === themes.length) {
88 | rows.push(`| [Add your theme][add-theme] | | |`);
89 | }
90 | }
91 |
92 | return rows.join("\n");
93 | };
94 |
95 | const buildReadme = () => {
96 | return THEME_TEMPLATE.split("\n")
97 | .map((line) => {
98 | if (line.includes(REPO_CARD_LINKS_FLAG)) {
99 | return generateLinks(createRepoMdLink);
100 | }
101 | if (line.includes(STAT_CARD_LINKS_FLAG)) {
102 | return generateLinks(createStatMdLink);
103 | }
104 | if (line.includes(REPO_CARD_TABLE_FLAG)) {
105 | return generateTable({ isRepoCard: true });
106 | }
107 | if (line.includes(STAT_CARD_TABLE_FLAG)) {
108 | return generateTable({ isRepoCard: false });
109 | }
110 | return line;
111 | })
112 | .join("\n");
113 | };
114 |
115 | fs.writeFileSync(TARGET_FILE, buildReadme());
116 |
--------------------------------------------------------------------------------
/scripts/push-theme-readme.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -x
3 | set -e
4 |
5 | export BRANCH_NAME=updated-theme-readme
6 | git --version
7 | git config --global user.email "no-reply@githubreadmestats.com"
8 | git config --global user.name "Github Readme Stats Bot"
9 | git branch -d $BRANCH_NAME || true
10 | git checkout -b $BRANCH_NAME
11 | git add --all
12 | git commit --message "docs(theme): Auto update theme readme" || exit 0
13 | git remote add origin-$BRANCH_NAME https://${PERSONAL_TOKEN}@github.com/${GH_REPO}.git
14 | git push --force --quiet --set-upstream origin-$BRANCH_NAME $BRANCH_NAME
--------------------------------------------------------------------------------
/src/calculateRank.js:
--------------------------------------------------------------------------------
1 | // https://stackoverflow.com/a/5263759/10629172
2 | function normalcdf(mean, sigma, to) {
3 | var z = (to - mean) / Math.sqrt(2 * sigma * sigma);
4 | var t = 1 / (1 + 0.3275911 * Math.abs(z));
5 | var a1 = 0.254829592;
6 | var a2 = -0.284496736;
7 | var a3 = 1.421413741;
8 | var a4 = -1.453152027;
9 | var a5 = 1.061405429;
10 | var erf =
11 | 1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-z * z);
12 | var sign = 1;
13 | if (z < 0) {
14 | sign = -1;
15 | }
16 | return (1 / 2) * (1 + sign * erf);
17 | }
18 |
19 | function calculateRank({
20 | totalRepos,
21 | totalCommits,
22 | contributions,
23 | followers,
24 | prs,
25 | issues,
26 | stargazers,
27 | }) {
28 | const COMMITS_OFFSET = 1.65;
29 | const CONTRIBS_OFFSET = 1.65;
30 | const ISSUES_OFFSET = 1;
31 | const STARS_OFFSET = 0.75;
32 | const PRS_OFFSET = 0.5;
33 | const FOLLOWERS_OFFSET = 0.45;
34 | const REPO_OFFSET = 1;
35 |
36 | const ALL_OFFSETS =
37 | CONTRIBS_OFFSET +
38 | ISSUES_OFFSET +
39 | STARS_OFFSET +
40 | PRS_OFFSET +
41 | FOLLOWERS_OFFSET +
42 | REPO_OFFSET;
43 |
44 | const RANK_S_VALUE = 1;
45 | const RANK_DOUBLE_A_VALUE = 25;
46 | const RANK_A2_VALUE = 45;
47 | const RANK_A3_VALUE = 60;
48 | const RANK_B_VALUE = 100;
49 |
50 | const TOTAL_VALUES =
51 | RANK_S_VALUE + RANK_A2_VALUE + RANK_A3_VALUE + RANK_B_VALUE;
52 |
53 | // prettier-ignore
54 | const score = (
55 | totalCommits * COMMITS_OFFSET +
56 | contributions * CONTRIBS_OFFSET +
57 | issues * ISSUES_OFFSET +
58 | stargazers * STARS_OFFSET +
59 | prs * PRS_OFFSET +
60 | followers * FOLLOWERS_OFFSET +
61 | totalRepos * REPO_OFFSET
62 | ) / 100;
63 |
64 | const normalizedScore = normalcdf(score, TOTAL_VALUES, ALL_OFFSETS) * 100;
65 |
66 | let level = "";
67 |
68 | if (normalizedScore < RANK_S_VALUE) {
69 | level = "S+";
70 | }
71 | if (
72 | normalizedScore >= RANK_S_VALUE &&
73 | normalizedScore < RANK_DOUBLE_A_VALUE
74 | ) {
75 | level = "S";
76 | }
77 | if (
78 | normalizedScore >= RANK_DOUBLE_A_VALUE &&
79 | normalizedScore < RANK_A2_VALUE
80 | ) {
81 | level = "A++";
82 | }
83 | if (normalizedScore >= RANK_A2_VALUE && normalizedScore < RANK_A3_VALUE) {
84 | level = "A+";
85 | }
86 | if (normalizedScore >= RANK_A3_VALUE && normalizedScore < RANK_B_VALUE) {
87 | level = "B+";
88 | }
89 |
90 | return { level, score: normalizedScore };
91 | }
92 |
93 | module.exports = calculateRank;
94 |
--------------------------------------------------------------------------------
/src/fetchRepo.js:
--------------------------------------------------------------------------------
1 | const { request } = require("./utils");
2 | const retryer = require("./retryer");
3 |
4 | const fetcher = (variables, token) => {
5 | return request(
6 | {
7 | query: `
8 | fragment RepoInfo on Repository {
9 | name
10 | nameWithOwner
11 | isPrivate
12 | isArchived
13 | isTemplate
14 | stargazers {
15 | totalCount
16 | }
17 | description
18 | primaryLanguage {
19 | color
20 | id
21 | name
22 | }
23 | forkCount
24 | }
25 | query getRepo($login: String!, $repo: String!) {
26 | user(login: $login) {
27 | repository(name: $repo) {
28 | ...RepoInfo
29 | }
30 | }
31 | organization(login: $login) {
32 | repository(name: $repo) {
33 | ...RepoInfo
34 | }
35 | }
36 | }
37 | `,
38 | variables,
39 | },
40 | {
41 | Authorization: `bearer ${token}`,
42 | }
43 | );
44 | };
45 |
46 | async function fetchRepo(username, reponame) {
47 | if (!username || !reponame) {
48 | throw new Error("Invalid username or reponame");
49 | }
50 |
51 | let res = await retryer(fetcher, { login: username, repo: reponame });
52 |
53 | const data = res.data.data;
54 |
55 | if (!data.user && !data.organization) {
56 | throw new Error("Not found");
57 | }
58 |
59 | const isUser = data.organization === null && data.user;
60 | const isOrg = data.user === null && data.organization;
61 |
62 | if (isUser) {
63 | if (!data.user.repository || data.user.repository.isPrivate) {
64 | throw new Error("User Repository Not found");
65 | }
66 | return data.user.repository;
67 | }
68 |
69 | if (isOrg) {
70 | if (
71 | !data.organization.repository ||
72 | data.organization.repository.isPrivate
73 | ) {
74 | throw new Error("Organization Repository Not found");
75 | }
76 | return data.organization.repository;
77 | }
78 | }
79 |
80 | module.exports = fetchRepo;
81 |
--------------------------------------------------------------------------------
/src/fetchStats.js:
--------------------------------------------------------------------------------
1 | const { request, logger } = require("./utils");
2 | const retryer = require("./retryer");
3 | const calculateRank = require("./calculateRank");
4 | require("dotenv").config();
5 |
6 | const fetcher = (variables, token) => {
7 | return request(
8 | {
9 | query: `
10 | query userInfo($login: String!) {
11 | user(login: $login) {
12 | name
13 | login
14 | contributionsCollection {
15 | totalCommitContributions
16 | restrictedContributionsCount
17 | }
18 | repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
19 | totalCount
20 | }
21 | pullRequests(first: 1) {
22 | totalCount
23 | }
24 | issues(first: 1) {
25 | totalCount
26 | }
27 | followers {
28 | totalCount
29 | }
30 | repositories(first: 100, ownerAffiliations: OWNER, isFork: false, orderBy: {direction: DESC, field: STARGAZERS}) {
31 | totalCount
32 | nodes {
33 | stargazers {
34 | totalCount
35 | }
36 | }
37 | }
38 | }
39 | }
40 | `,
41 | variables,
42 | },
43 | {
44 | Authorization: `bearer ${token}`,
45 | }
46 | );
47 | };
48 |
49 | async function fetchStats(username, count_private = false) {
50 | if (!username) throw Error("Invalid username");
51 |
52 | const stats = {
53 | name: "",
54 | totalPRs: 0,
55 | totalCommits: 0,
56 | totalIssues: 0,
57 | totalStars: 0,
58 | contributedTo: 0,
59 | rank: { level: "C", score: 0 },
60 | };
61 |
62 | let res = await retryer(fetcher, { login: username });
63 |
64 | if (res.data.errors) {
65 | logger.error(res.data.errors);
66 | throw Error(res.data.errors[0].message || "Could not fetch user");
67 | }
68 |
69 | const user = res.data.data.user;
70 | const contributionCount = user.contributionsCollection;
71 |
72 | stats.name = user.name || user.login;
73 | stats.totalIssues = user.issues.totalCount;
74 |
75 | stats.totalCommits = contributionCount.totalCommitContributions;
76 | if (count_private) {
77 | stats.totalCommits =
78 | contributionCount.totalCommitContributions +
79 | contributionCount.restrictedContributionsCount;
80 | }
81 |
82 | stats.totalPRs = user.pullRequests.totalCount;
83 | stats.contributedTo = user.repositoriesContributedTo.totalCount;
84 |
85 | stats.totalStars = user.repositories.nodes.reduce((prev, curr) => {
86 | return prev + curr.stargazers.totalCount;
87 | }, 0);
88 |
89 | stats.rank = calculateRank({
90 | totalCommits: stats.totalCommits,
91 | totalRepos: user.repositories.totalCount,
92 | followers: user.followers.totalCount,
93 | contributions: stats.contributedTo,
94 | stargazers: stats.totalStars,
95 | prs: stats.totalPRs,
96 | issues: stats.totalIssues,
97 | });
98 |
99 | return stats;
100 | }
101 |
102 | module.exports = fetchStats;
103 |
--------------------------------------------------------------------------------
/src/fetchTopLanguages.js:
--------------------------------------------------------------------------------
1 | const { request, logger } = require("./utils");
2 | const retryer = require("./retryer");
3 | require("dotenv").config();
4 |
5 | const fetcher = (variables, token) => {
6 | return request(
7 | {
8 | query: `
9 | query userInfo($login: String!) {
10 | user(login: $login) {
11 | # fetch only owner repos & not forks
12 | repositories(ownerAffiliations: OWNER, isFork: false, first: 100) {
13 | nodes {
14 | languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {
15 | edges {
16 | size
17 | node {
18 | color
19 | name
20 | }
21 | }
22 | }
23 | }
24 | }
25 | }
26 | }
27 | `,
28 | variables,
29 | },
30 | {
31 | Authorization: `bearer ${token}`,
32 | }
33 | );
34 | };
35 |
36 | async function fetchTopLanguages(username) {
37 | if (!username) throw Error("Invalid username");
38 |
39 | let res = await retryer(fetcher, { login: username });
40 |
41 | if (res.data.errors) {
42 | logger.error(res.data.errors);
43 | throw Error(res.data.errors[0].message || "Could not fetch user");
44 | }
45 |
46 | let repoNodes = res.data.data.user.repositories.nodes;
47 |
48 | repoNodes = repoNodes
49 | .filter((node) => {
50 | return node.languages.edges.length > 0;
51 | })
52 | // flatten the list of language nodes
53 | .reduce((acc, curr) => curr.languages.edges.concat(acc), [])
54 | .sort((a, b) => b.size - a.size)
55 | .reduce((acc, prev) => {
56 | // get the size of the language (bytes)
57 | let langSize = prev.size;
58 |
59 | // if we already have the language in the accumulator
60 | // & the current language name is same as previous name
61 | // add the size to the language size.
62 | if (acc[prev.node.name] && prev.node.name === acc[prev.node.name].name) {
63 | langSize = prev.size + acc[prev.node.name].size;
64 | }
65 | return {
66 | ...acc,
67 | [prev.node.name]: {
68 | name: prev.node.name,
69 | color: prev.node.color,
70 | size: langSize,
71 | },
72 | };
73 | }, {});
74 |
75 | const topLangs = Object.keys(repoNodes)
76 | .slice(0, 5)
77 | .reduce((result, key) => {
78 | result[key] = repoNodes[key];
79 | return result;
80 | }, {});
81 |
82 | return topLangs;
83 | }
84 |
85 | module.exports = fetchTopLanguages;
86 |
--------------------------------------------------------------------------------
/src/getStyles.js:
--------------------------------------------------------------------------------
1 | const calculateCircleProgress = (value) => {
2 | let radius = 40;
3 | let c = Math.PI * (radius * 2);
4 |
5 | if (value < 0) value = 0;
6 | if (value > 100) value = 100;
7 |
8 | let percentage = ((100 - value) / 100) * c;
9 | return percentage;
10 | };
11 |
12 | const getAnimations = ({ progress }) => {
13 | return `
14 | /* Animations */
15 | @keyframes scaleIn {
16 | from {
17 | transform: translate(-5px, 5px) scale(0);
18 | }
19 | to {
20 | transform: translate(-5px, 5px) scale(1);
21 | }
22 | }
23 | @keyframes fadeIn {
24 | from {
25 | opacity: 0;
26 | }
27 | to {
28 | opacity: 1;
29 | }
30 | }
31 | @keyframes rankAnimation {
32 | from {
33 | stroke-dashoffset: ${calculateCircleProgress(0)};
34 | }
35 | to {
36 | stroke-dashoffset: ${calculateCircleProgress(progress)};
37 | }
38 | }
39 | `;
40 | };
41 |
42 | const getStyles = ({
43 | titleColor,
44 | textColor,
45 | iconColor,
46 | show_icons,
47 | progress,
48 | }) => {
49 | return `
50 | .header {
51 | font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor};
52 | animation: fadeIn 0.8s ease-in-out forwards;
53 | }
54 | .stat {
55 | font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
56 | }
57 | .stagger {
58 | opacity: 0;
59 | animation: fadeIn 0.3s ease-in-out forwards;
60 | }
61 | .rank-text {
62 | font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor};
63 | animation: scaleIn 0.3s ease-in-out forwards;
64 | }
65 |
66 | .bold { font-weight: 700 }
67 | .icon {
68 | fill: ${iconColor};
69 | display: ${!!show_icons ? "block" : "none"};
70 | }
71 |
72 | .rank-circle-rim {
73 | stroke: ${titleColor};
74 | fill: none;
75 | stroke-width: 6;
76 | opacity: 0.2;
77 | }
78 | .rank-circle {
79 | stroke: ${titleColor};
80 | stroke-dasharray: 250;
81 | fill: none;
82 | stroke-width: 6;
83 | stroke-linecap: round;
84 | opacity: 0.8;
85 | transform-origin: -10px 8px;
86 | transform: rotate(-90deg);
87 | animation: rankAnimation 1s forwards ease-in-out;
88 | }
89 |
90 | ${process.env.NODE_ENV === "test" ? "" : getAnimations({ progress })}
91 | `;
92 | };
93 |
94 | module.exports = getStyles;
95 |
--------------------------------------------------------------------------------
/src/icons.js:
--------------------------------------------------------------------------------
1 | const icons = {
2 | star: ` `,
3 | commits: ` `,
4 | prs: ` `,
5 | issues: ` `,
6 | icon: ` `,
7 | contribs: ` `,
8 | fork: ` `,
9 | };
10 |
11 | module.exports = icons;
12 |
--------------------------------------------------------------------------------
/src/renderRepoCard.js:
--------------------------------------------------------------------------------
1 | const {
2 | kFormatter,
3 | encodeHTML,
4 | getCardColors,
5 | FlexLayout,
6 | wrapTextMultiline,
7 | } = require("../src/utils");
8 | const icons = require("./icons");
9 | const toEmoji = require("emoji-name-map");
10 |
11 | const renderRepoCard = (repo, options = {}) => {
12 | const {
13 | name,
14 | nameWithOwner,
15 | description,
16 | primaryLanguage,
17 | stargazers,
18 | isArchived,
19 | isTemplate,
20 | forkCount,
21 | } = repo;
22 | const {
23 | title_color,
24 | icon_color,
25 | text_color,
26 | bg_color,
27 | show_owner,
28 | theme = "default_repocard",
29 | } = options;
30 |
31 | const header = show_owner ? nameWithOwner : name;
32 | const langName = (primaryLanguage && primaryLanguage.name) || "Unspecified";
33 | const langColor = (primaryLanguage && primaryLanguage.color) || "#333";
34 |
35 | const shiftText = langName.length > 15 ? 0 : 30;
36 |
37 | let desc = description || "No description provided";
38 |
39 | // parse emojis to unicode
40 | desc = desc.replace(/:\w+:/gm, (emoji) => {
41 | return toEmoji.get(emoji) || "";
42 | });
43 |
44 | const multiLineDescription = wrapTextMultiline(desc);
45 | const descriptionLines = multiLineDescription.length;
46 | const lineHeight = 10;
47 |
48 | const height =
49 | (descriptionLines > 1 ? 120 : 110) + descriptionLines * lineHeight;
50 |
51 | // returns theme based colors with proper overrides and defaults
52 | const { titleColor, textColor, iconColor, bgColor } = getCardColors({
53 | title_color,
54 | icon_color,
55 | text_color,
56 | bg_color,
57 | theme,
58 | });
59 |
60 | const totalStars = kFormatter(stargazers.totalCount);
61 | const totalForks = kFormatter(forkCount);
62 |
63 | const getBadgeSVG = (label) => `
64 |
65 |
66 |
73 | ${label}
74 |
75 |
76 | `;
77 |
78 | const svgLanguage = primaryLanguage
79 | ? `
80 |
81 |
82 | ${langName}
83 |
84 | `
85 | : "";
86 |
87 | const svgStars =
88 | stargazers.totalCount > 0 &&
89 | `
90 |
91 | ${icons.star}
92 |
93 | ${totalStars}
94 | `;
95 |
96 | const svgForks =
97 | forkCount > 0 &&
98 | `
99 |
100 | ${icons.fork}
101 |
102 | ${totalForks}
103 | `;
104 |
105 | return `
106 |
107 |
115 |
116 |
117 |
118 | ${icons.contribs}
119 |
120 |
121 |
122 |
123 | ${
124 | isTemplate
125 | ? getBadgeSVG("Template")
126 | : isArchived
127 | ? getBadgeSVG("Archived")
128 | : ""
129 | }
130 |
131 |
132 | ${multiLineDescription
133 | .map((line) => `${encodeHTML(line)} `)
134 | .join("")}
135 |
136 |
137 |
138 | ${svgLanguage}
139 |
140 |
144 | ${FlexLayout({ items: [svgStars, svgForks], gap: 65 }).join("")}
145 |
146 |
147 |
148 | `;
149 | };
150 |
151 | module.exports = renderRepoCard;
152 |
--------------------------------------------------------------------------------
/src/renderStatsCard.js:
--------------------------------------------------------------------------------
1 | const {
2 | kFormatter,
3 | getCardColors,
4 | FlexLayout,
5 | encodeHTML,
6 | } = require("../src/utils");
7 | const getStyles = require("./getStyles");
8 | const icons = require("./icons");
9 |
10 | const createTextNode = ({ icon, label, value, id, index, showIcons }) => {
11 | const kValue = kFormatter(value);
12 | const staggerDelay = (index + 3) * 150;
13 |
14 | const labelOffset = showIcons ? `x="25"` : "";
15 | const iconSvg = showIcons
16 | ? `
17 |
18 | ${icon}
19 |
20 | `
21 | : "";
22 | return `
23 |
24 | ${iconSvg}
25 | ${label}:
26 | ${kValue}
27 |
28 | `;
29 | };
30 |
31 | const renderStatsCard = (stats = {}, options = { hide: [] }) => {
32 | const {
33 | name,
34 | totalStars,
35 | totalCommits,
36 | totalIssues,
37 | totalPRs,
38 | contributedTo,
39 | rank,
40 | } = stats;
41 | const {
42 | hide = [],
43 | show_icons = false,
44 | hide_title = false,
45 | hide_border = false,
46 | hide_rank = false,
47 | line_height = 25,
48 | title_color,
49 | icon_color,
50 | text_color,
51 | bg_color,
52 | theme = "default",
53 | } = options;
54 |
55 | const lheight = parseInt(line_height);
56 |
57 | // returns theme based colors with proper overrides and defaults
58 | const { titleColor, textColor, iconColor, bgColor } = getCardColors({
59 | title_color,
60 | icon_color,
61 | text_color,
62 | bg_color,
63 | theme,
64 | });
65 |
66 | // Meta data for creating text nodes with createTextNode function
67 | const STATS = {
68 | stars: {
69 | icon: icons.star,
70 | label: "Total Stars",
71 | value: totalStars,
72 | id: "stars",
73 | },
74 | commits: {
75 | icon: icons.commits,
76 | label: "Total Commits",
77 | value: totalCommits,
78 | id: "commits",
79 | },
80 | prs: {
81 | icon: icons.prs,
82 | label: "Total PRs",
83 | value: totalPRs,
84 | id: "prs",
85 | },
86 | issues: {
87 | icon: icons.issues,
88 | label: "Total Issues",
89 | value: totalIssues,
90 | id: "issues",
91 | },
92 | contribs: {
93 | icon: icons.contribs,
94 | label: "Contributed to",
95 | value: contributedTo,
96 | id: "contribs",
97 | },
98 | };
99 |
100 | // filter out hidden stats defined by user & create the text nodes
101 | const statItems = Object.keys(STATS)
102 | .filter((key) => !hide.includes(key))
103 | .map((key, index) =>
104 | // create the text nodes, and pass index so that we can calculate the line spacing
105 | createTextNode({
106 | ...STATS[key],
107 | index,
108 | showIcons: show_icons,
109 | })
110 | );
111 |
112 | // Calculate the card height depending on how many items there are
113 | // but if rank circle is visible clamp the minimum height to `150`
114 | let height = Math.max(
115 | 45 + (statItems.length + 1) * lheight,
116 | hide_rank ? 0 : 150
117 | );
118 |
119 | // the better user's score the the rank will be closer to zero so
120 | // subtracting 100 to get the progress in 100%
121 | const progress = 100 - rank.score;
122 |
123 | const styles = getStyles({
124 | titleColor,
125 | textColor,
126 | iconColor,
127 | show_icons,
128 | progress,
129 | });
130 |
131 | // Conditionally rendered elements
132 |
133 | const apostrophe = ["x", "s"].includes(name.slice(-1)) ? "" : "s";
134 | const title = hide_title
135 | ? ""
136 | : ``;
137 |
138 | const border = `
139 |
150 | `;
151 |
152 | const rankCircle = hide_rank
153 | ? ""
154 | : `
157 |
158 |
159 |
160 |
167 | ${rank.level}
168 |
169 |
170 | `;
171 |
172 | if (hide_title) {
173 | height -= 30;
174 | }
175 |
176 | return `
177 |
178 |
181 |
182 | ${border}
183 | ${title}
184 |
185 |
188 | ${rankCircle}
189 |
190 |
191 | ${FlexLayout({
192 | items: statItems,
193 | gap: lheight,
194 | direction: "column",
195 | }).join("")}
196 |
197 |
198 |
199 | `;
200 | };
201 |
202 | module.exports = renderStatsCard;
203 |
--------------------------------------------------------------------------------
/src/renderTopLanguages.js:
--------------------------------------------------------------------------------
1 | const { getCardColors, FlexLayout, clampValue } = require("../src/utils");
2 |
3 | const createProgressNode = ({ width, color, name, progress }) => {
4 | const paddingRight = 95;
5 | const progressTextX = width - paddingRight + 10;
6 | const progressWidth = width - paddingRight;
7 | const progressPercentage = clampValue(progress, 2, 100);
8 |
9 | return `
10 | ${name}
11 | ${progress}%
12 |
13 |
14 |
21 |
22 |
23 | `;
24 | };
25 |
26 | const createCompactLangNode = ({ lang, totalSize, x, y }) => {
27 | const percentage = ((lang.size / totalSize) * 100).toFixed(2);
28 | const color = lang.color || "#858585";
29 |
30 | return `
31 |
32 |
33 |
34 | ${lang.name} ${percentage}%
35 |
36 |
37 | `;
38 | };
39 |
40 | const createLanguageTextNode = ({ langs, totalSize, x, y }) => {
41 | return langs.map((lang, index) => {
42 | if (index % 2 === 0) {
43 | return createCompactLangNode({
44 | lang,
45 | x,
46 | y: 12.5 * index + y,
47 | totalSize,
48 | index,
49 | });
50 | }
51 | return createCompactLangNode({
52 | lang,
53 | x: 150,
54 | y: 12.5 + 12.5 * index,
55 | totalSize,
56 | index,
57 | });
58 | });
59 | };
60 |
61 | const lowercaseTrim = (name) => name.toLowerCase().trim();
62 |
63 | const renderTopLanguages = (topLangs, options = {}) => {
64 | const {
65 | hide_title,
66 | card_width,
67 | title_color,
68 | text_color,
69 | bg_color,
70 | hide,
71 | theme,
72 | layout,
73 | } = options;
74 |
75 | let langs = Object.values(topLangs);
76 | let langsToHide = {};
77 |
78 | // populate langsToHide map for quick lookup
79 | // while filtering out
80 | if (hide) {
81 | hide.forEach((langName) => {
82 | langsToHide[lowercaseTrim(langName)] = true;
83 | });
84 | }
85 |
86 | // filter out langauges to be hidden
87 | langs = langs
88 | .sort((a, b) => b.size - a.size)
89 | .filter((lang) => {
90 | return !langsToHide[lowercaseTrim(lang.name)];
91 | });
92 |
93 | const totalLanguageSize = langs.reduce((acc, curr) => {
94 | return acc + curr.size;
95 | }, 0);
96 |
97 | // returns theme based colors with proper overrides and defaults
98 | const { titleColor, textColor, bgColor } = getCardColors({
99 | title_color,
100 | text_color,
101 | bg_color,
102 | theme,
103 | });
104 |
105 | let width = isNaN(card_width) ? 300 : card_width;
106 | let height = 45 + (langs.length + 1) * 40;
107 |
108 | let finalLayout = "";
109 |
110 | // RENDER COMPACT LAYOUT
111 | if (layout === "compact") {
112 | width = width + 50;
113 | height = 30 + (langs.length / 2 + 1) * 40;
114 |
115 | // progressOffset holds the previous language's width and used to offset the next language
116 | // so that we can stack them one after another, like this: [--][----][---]
117 | let progressOffset = 0;
118 | const compactProgressBar = langs
119 | .map((lang) => {
120 | const percentage = (
121 | (lang.size / totalLanguageSize) *
122 | (width - 50)
123 | ).toFixed(2);
124 |
125 | const progress =
126 | percentage < 10 ? parseFloat(percentage) + 10 : percentage;
127 |
128 | const output = `
129 |
138 | `;
139 | progressOffset += parseFloat(percentage);
140 | return output;
141 | })
142 | .join("");
143 |
144 | finalLayout = `
145 |
146 |
149 |
150 | ${compactProgressBar}
151 | ${createLanguageTextNode({
152 | x: 0,
153 | y: 25,
154 | langs,
155 | totalSize: totalLanguageSize,
156 | }).join("")}
157 | `;
158 | } else {
159 | finalLayout = FlexLayout({
160 | items: langs.map((lang) => {
161 | return createProgressNode({
162 | width: width,
163 | name: lang.name,
164 | color: lang.color || "#858585",
165 | progress: ((lang.size / totalLanguageSize) * 100).toFixed(2),
166 | });
167 | }),
168 | gap: 40,
169 | direction: "column",
170 | }).join("");
171 | }
172 |
173 | if (hide_title) {
174 | height -= 30;
175 | }
176 |
177 | return `
178 |
179 |
183 |
184 |
185 | ${
186 | hide_title
187 | ? ""
188 | : ``
189 | }
190 |
191 |
192 | ${finalLayout}
193 |
194 |
195 | `;
196 | };
197 |
198 | module.exports = renderTopLanguages;
199 |
--------------------------------------------------------------------------------
/src/retryer.js:
--------------------------------------------------------------------------------
1 | const { logger } = require("./utils");
2 |
3 | const retryer = async (fetcher, variables, retries = 0) => {
4 | if (retries > 7) {
5 | throw new Error("Maximum retries exceeded");
6 | }
7 | try {
8 | logger.log(`Trying PAT_${retries + 1}`);
9 |
10 | // try to fetch with the first token since RETRIES is 0 index i'm adding +1
11 | let response = await fetcher(
12 | variables,
13 | process.env[`PAT_${retries + 1}`],
14 | retries
15 | );
16 |
17 | // prettier-ignore
18 | const isRateExceeded = response.data.errors && response.data.errors[0].type === "RATE_LIMITED";
19 |
20 | // if rate limit is hit increase the RETRIES and recursively call the retryer
21 | // with username, and current RETRIES
22 | if (isRateExceeded) {
23 | logger.log(`PAT_${retries + 1} Failed`);
24 | retries++;
25 | // directly return from the function
26 | return retryer(fetcher, variables, retries);
27 | }
28 |
29 | // finally return the response
30 | return response;
31 | } catch (err) {
32 | // prettier-ignore
33 | // also checking for bad credentials if any tokens gets invalidated
34 | const isBadCredential = err.response.data && err.response.data.message === "Bad credentials";
35 |
36 | if (isBadCredential) {
37 | logger.log(`PAT_${retries + 1} Failed`);
38 | retries++;
39 | // directly return from the function
40 | return retryer(fetcher, variables, retries);
41 | }
42 | }
43 | };
44 |
45 | module.exports = retryer;
46 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | const axios = require("axios");
2 | const wrap = require("word-wrap");
3 | const themes = require("../themes");
4 |
5 | const renderError = (message, secondaryMessage = "") => {
6 | return `
7 |
8 |
13 |
14 | Something went wrong! file an issue at https://git.io/JJmN9
15 |
16 | ${encodeHTML(message)}
17 | ${secondaryMessage}
18 |
19 |
20 | `;
21 | };
22 |
23 | // https://stackoverflow.com/a/48073476/10629172
24 | function encodeHTML(str) {
25 | return str
26 | .replace(/[\u00A0-\u9999<>&](?!#)/gim, (i) => {
27 | return "" + i.charCodeAt(0) + ";";
28 | })
29 | .replace(/\u0008/gim, "");
30 | }
31 |
32 | function kFormatter(num) {
33 | return Math.abs(num) > 999
34 | ? Math.sign(num) * (Math.abs(num) / 1000).toFixed(1) + "k"
35 | : Math.sign(num) * Math.abs(num);
36 | }
37 |
38 | function isValidHexColor(hexColor) {
39 | return new RegExp(
40 | /^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4})$/
41 | ).test(hexColor);
42 | }
43 |
44 | function parseBoolean(value) {
45 | if (value === "true") {
46 | return true;
47 | } else if (value === "false") {
48 | return false;
49 | } else {
50 | return value;
51 | }
52 | }
53 |
54 | function parseArray(str) {
55 | if (!str) return [];
56 | return str.split(",");
57 | }
58 |
59 | function clampValue(number, min, max) {
60 | return Math.max(min, Math.min(number, max));
61 | }
62 |
63 | function fallbackColor(color, fallbackColor) {
64 | return (isValidHexColor(color) && `#${color}`) || fallbackColor;
65 | }
66 |
67 | function request(data, headers) {
68 | return axios({
69 | url: "https://api.github.com/graphql",
70 | method: "post",
71 | headers,
72 | data,
73 | });
74 | }
75 |
76 | /**
77 | *
78 | * @param {String[]} items
79 | * @param {Number} gap
80 | * @param {string} direction
81 | *
82 | * @description
83 | * Auto layout utility, allows us to layout things
84 | * vertically or horizontally with proper gaping
85 | */
86 | function FlexLayout({ items, gap, direction }) {
87 | // filter() for filtering out empty strings
88 | return items.filter(Boolean).map((item, i) => {
89 | let transform = `translate(${gap * i}, 0)`;
90 | if (direction === "column") {
91 | transform = `translate(0, ${gap * i})`;
92 | }
93 | return `${item} `;
94 | });
95 | }
96 |
97 | // returns theme based colors with proper overrides and defaults
98 | function getCardColors({
99 | title_color,
100 | text_color,
101 | icon_color,
102 | bg_color,
103 | theme,
104 | fallbackTheme = "default",
105 | }) {
106 | const defaultTheme = themes[fallbackTheme];
107 | const selectedTheme = themes[theme] || defaultTheme;
108 |
109 | // get the color provided by the user else the theme color
110 | // finally if both colors are invalid fallback to default theme
111 | const titleColor = fallbackColor(
112 | title_color || selectedTheme.title_color,
113 | "#" + defaultTheme.title_color
114 | );
115 | const iconColor = fallbackColor(
116 | icon_color || selectedTheme.icon_color,
117 | "#" + defaultTheme.icon_color
118 | );
119 | const textColor = fallbackColor(
120 | text_color || selectedTheme.text_color,
121 | "#" + defaultTheme.text_color
122 | );
123 | const bgColor = fallbackColor(
124 | bg_color || selectedTheme.bg_color,
125 | "#" + defaultTheme.bg_color
126 | );
127 |
128 | return { titleColor, iconColor, textColor, bgColor };
129 | }
130 |
131 | function wrapTextMultiline(text, width = 60, maxLines = 3) {
132 | const wrapped = wrap(encodeHTML(text), { width })
133 | .split("\n") // Split wrapped lines to get an array of lines
134 | .map((line) => line.trim()); // Remove leading and trailing whitespace of each line
135 |
136 | const lines = wrapped.slice(0, maxLines); // Only consider maxLines lines
137 |
138 | // Add "..." to the last line if the text exceeds maxLines
139 | if (wrapped.length > maxLines) {
140 | lines[maxLines - 1] += "...";
141 | }
142 |
143 | // Remove empty lines if text fits in less than maxLines lines
144 | const multiLineText = lines.filter(Boolean);
145 | return multiLineText;
146 | }
147 |
148 | const noop = () => {};
149 | // return console instance based on the environment
150 | const logger =
151 | process.env.NODE_ENV !== "test" ? console : { log: noop, error: noop };
152 |
153 | const CONSTANTS = {
154 | THIRTY_MINUTES: 1800,
155 | TWO_HOURS: 7200,
156 | ONE_DAY: 86400,
157 | };
158 |
159 | module.exports = {
160 | renderError,
161 | kFormatter,
162 | encodeHTML,
163 | isValidHexColor,
164 | request,
165 | parseArray,
166 | parseBoolean,
167 | fallbackColor,
168 | FlexLayout,
169 | getCardColors,
170 | clampValue,
171 | wrapTextMultiline,
172 | logger,
173 | CONSTANTS,
174 | };
175 |
--------------------------------------------------------------------------------
/tests/api.test.js:
--------------------------------------------------------------------------------
1 | require("@testing-library/jest-dom");
2 | const axios = require("axios");
3 | const MockAdapter = require("axios-mock-adapter");
4 | const api = require("../api/index");
5 | const renderStatsCard = require("../src/renderStatsCard");
6 | const { renderError, CONSTANTS } = require("../src/utils");
7 | const calculateRank = require("../src/calculateRank");
8 |
9 | const stats = {
10 | name: "Anurag Hazra",
11 | totalStars: 100,
12 | totalCommits: 200,
13 | totalIssues: 300,
14 | totalPRs: 400,
15 | contributedTo: 500,
16 | rank: null,
17 | };
18 | stats.rank = calculateRank({
19 | totalCommits: stats.totalCommits,
20 | totalRepos: 1,
21 | followers: 0,
22 | contributions: stats.contributedTo,
23 | stargazers: stats.totalStars,
24 | prs: stats.totalPRs,
25 | issues: stats.totalIssues,
26 | });
27 |
28 | const data = {
29 | data: {
30 | user: {
31 | name: stats.name,
32 | repositoriesContributedTo: { totalCount: stats.contributedTo },
33 | contributionsCollection: {
34 | totalCommitContributions: stats.totalCommits,
35 | restrictedContributionsCount: 100,
36 | },
37 | pullRequests: { totalCount: stats.totalPRs },
38 | issues: { totalCount: stats.totalIssues },
39 | followers: { totalCount: 0 },
40 | repositories: {
41 | totalCount: 1,
42 | nodes: [{ stargazers: { totalCount: 100 } }],
43 | },
44 | },
45 | },
46 | };
47 |
48 | const error = {
49 | errors: [
50 | {
51 | type: "NOT_FOUND",
52 | path: ["user"],
53 | locations: [],
54 | message: "Could not fetch user",
55 | },
56 | ],
57 | };
58 |
59 | const mock = new MockAdapter(axios);
60 |
61 | const faker = (query, data) => {
62 | const req = {
63 | query: {
64 | username: "anuraghazra",
65 | ...query,
66 | },
67 | };
68 | const res = {
69 | setHeader: jest.fn(),
70 | send: jest.fn(),
71 | };
72 | mock.onPost("https://api.github.com/graphql").reply(200, data);
73 |
74 | return { req, res };
75 | };
76 |
77 | afterEach(() => {
78 | mock.reset();
79 | });
80 |
81 | describe("Test /api/", () => {
82 | it("should test the request", async () => {
83 | const { req, res } = faker({}, data);
84 |
85 | await api(req, res);
86 |
87 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
88 | expect(res.send).toBeCalledWith(renderStatsCard(stats, { ...req.query }));
89 | });
90 |
91 | it("should render error card on error", async () => {
92 | const { req, res } = faker({}, error);
93 |
94 | await api(req, res);
95 |
96 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
97 | expect(res.send).toBeCalledWith(
98 | renderError(
99 | error.errors[0].message,
100 | "Make sure the provided username is not an organization"
101 | )
102 | );
103 | });
104 |
105 | it("should get the query options", async () => {
106 | const { req, res } = faker(
107 | {
108 | username: "anuraghazra",
109 | hide: "issues,prs,contribs",
110 | show_icons: true,
111 | hide_border: true,
112 | line_height: 100,
113 | title_color: "fff",
114 | icon_color: "fff",
115 | text_color: "fff",
116 | bg_color: "fff",
117 | },
118 | data
119 | );
120 |
121 | await api(req, res);
122 |
123 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
124 | expect(res.send).toBeCalledWith(
125 | renderStatsCard(stats, {
126 | hide: ["issues", "prs", "contribs"],
127 | show_icons: true,
128 | hide_border: true,
129 | line_height: 100,
130 | title_color: "fff",
131 | icon_color: "fff",
132 | text_color: "fff",
133 | bg_color: "fff",
134 | })
135 | );
136 | });
137 |
138 | it("should have proper cache", async () => {
139 | const { req, res } = faker({}, data);
140 | mock.onPost("https://api.github.com/graphql").reply(200, data);
141 |
142 | await api(req, res);
143 |
144 | expect(res.setHeader.mock.calls).toEqual([
145 | ["Content-Type", "image/svg+xml"],
146 | ["Cache-Control", `public, max-age=${CONSTANTS.THIRTY_MINUTES}`],
147 | ]);
148 | });
149 |
150 | it("should set proper cache", async () => {
151 | const { req, res } = faker({ cache_seconds: 2000 }, data);
152 | await api(req, res);
153 |
154 | expect(res.setHeader.mock.calls).toEqual([
155 | ["Content-Type", "image/svg+xml"],
156 | ["Cache-Control", `public, max-age=${2000}`],
157 | ]);
158 | });
159 |
160 | it("should set proper cache with clamped values", async () => {
161 | {
162 | let { req, res } = faker({ cache_seconds: 200000 }, data);
163 | await api(req, res);
164 |
165 | expect(res.setHeader.mock.calls).toEqual([
166 | ["Content-Type", "image/svg+xml"],
167 | ["Cache-Control", `public, max-age=${CONSTANTS.ONE_DAY}`],
168 | ]);
169 | }
170 |
171 | // note i'm using block scoped vars
172 | {
173 | let { req, res } = faker({ cache_seconds: 0 }, data);
174 | await api(req, res);
175 |
176 | expect(res.setHeader.mock.calls).toEqual([
177 | ["Content-Type", "image/svg+xml"],
178 | ["Cache-Control", `public, max-age=${CONSTANTS.THIRTY_MINUTES}`],
179 | ]);
180 | }
181 |
182 | {
183 | let { req, res } = faker({ cache_seconds: -10000 }, data);
184 | await api(req, res);
185 |
186 | expect(res.setHeader.mock.calls).toEqual([
187 | ["Content-Type", "image/svg+xml"],
188 | ["Cache-Control", `public, max-age=${CONSTANTS.THIRTY_MINUTES}`],
189 | ]);
190 | }
191 | });
192 |
193 | it("should add private contributions", async () => {
194 | const { req, res } = faker(
195 | {
196 | username: "anuraghazra",
197 | count_private: true,
198 | },
199 | data
200 | );
201 |
202 | await api(req, res);
203 |
204 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
205 | expect(res.send).toBeCalledWith(
206 | renderStatsCard(
207 | {
208 | ...stats,
209 | totalCommits: stats.totalCommits + 100,
210 | rank: calculateRank({
211 | totalCommits: stats.totalCommits + 100,
212 | totalRepos: 1,
213 | followers: 0,
214 | contributions: stats.contributedTo,
215 | stargazers: stats.totalStars,
216 | prs: stats.totalPRs,
217 | issues: stats.totalIssues,
218 | }),
219 | },
220 | {}
221 | )
222 | );
223 | });
224 | });
225 |
--------------------------------------------------------------------------------
/tests/calculateRank.test.js:
--------------------------------------------------------------------------------
1 | require("@testing-library/jest-dom");
2 | const calculateRank = require("../src/calculateRank");
3 |
4 | describe("Test calculateRank", () => {
5 | it("should calculate rank correctly", () => {
6 | expect(
7 | calculateRank({
8 | totalCommits: 100,
9 | totalRepos: 5,
10 | followers: 100,
11 | contributions: 61,
12 | stargazers: 400,
13 | prs: 300,
14 | issues: 200,
15 | })
16 | ).toStrictEqual({ level: "A+", score: 49.16605417270399 });
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/tests/fetchRepo.test.js:
--------------------------------------------------------------------------------
1 | require("@testing-library/jest-dom");
2 | const axios = require("axios");
3 | const MockAdapter = require("axios-mock-adapter");
4 | const fetchRepo = require("../src/fetchRepo");
5 |
6 | const data_repo = {
7 | repository: {
8 | name: "convoychat",
9 | stargazers: { totalCount: 38000 },
10 | description: "Help us take over the world! React + TS + GraphQL Chat App",
11 | primaryLanguage: {
12 | color: "#2b7489",
13 | id: "MDg6TGFuZ3VhZ2UyODc=",
14 | name: "TypeScript",
15 | },
16 | forkCount: 100,
17 | },
18 | };
19 |
20 | const data_user = {
21 | data: {
22 | user: { repository: data_repo },
23 | organization: null,
24 | },
25 | };
26 | const data_org = {
27 | data: {
28 | user: null,
29 | organization: { repository: data_repo },
30 | },
31 | };
32 |
33 | const mock = new MockAdapter(axios);
34 |
35 | afterEach(() => {
36 | mock.reset();
37 | });
38 |
39 | describe("Test fetchRepo", () => {
40 | it("should fetch correct user repo", async () => {
41 | mock.onPost("https://api.github.com/graphql").reply(200, data_user);
42 |
43 | let repo = await fetchRepo("anuraghazra", "convoychat");
44 | expect(repo).toStrictEqual(data_repo);
45 | });
46 |
47 | it("should fetch correct org repo", async () => {
48 | mock.onPost("https://api.github.com/graphql").reply(200, data_org);
49 |
50 | let repo = await fetchRepo("anuraghazra", "convoychat");
51 | expect(repo).toStrictEqual(data_repo);
52 | });
53 |
54 | it("should throw error if user is found but repo is null", async () => {
55 | mock
56 | .onPost("https://api.github.com/graphql")
57 | .reply(200, { data: { user: { repository: null }, organization: null } });
58 |
59 | await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow(
60 | "User Repository Not found"
61 | );
62 | });
63 |
64 | it("should throw error if org is found but repo is null", async () => {
65 | mock
66 | .onPost("https://api.github.com/graphql")
67 | .reply(200, { data: { user: null, organization: { repository: null } } });
68 |
69 | await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow(
70 | "Organization Repository Not found"
71 | );
72 | });
73 |
74 | it("should throw error if both user & org data not found", async () => {
75 | mock
76 | .onPost("https://api.github.com/graphql")
77 | .reply(200, { data: { user: null, organization: null } });
78 |
79 | await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow(
80 | "Not found"
81 | );
82 | });
83 |
84 | it("should throw error if repository is private", async () => {
85 | mock.onPost("https://api.github.com/graphql").reply(200, {
86 | data: {
87 | user: { repository: { ...data_repo, isPrivate: true } },
88 | organization: null,
89 | },
90 | });
91 |
92 | await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow(
93 | "User Repository Not found"
94 | );
95 | });
96 | });
97 |
--------------------------------------------------------------------------------
/tests/fetchStats.test.js:
--------------------------------------------------------------------------------
1 | require("@testing-library/jest-dom");
2 | const axios = require("axios");
3 | const MockAdapter = require("axios-mock-adapter");
4 | const fetchStats = require("../src/fetchStats");
5 | const calculateRank = require("../src/calculateRank");
6 |
7 | const data = {
8 | data: {
9 | user: {
10 | name: "Anurag Hazra",
11 | repositoriesContributedTo: { totalCount: 61 },
12 | contributionsCollection: { totalCommitContributions: 100, restrictedContributionsCount: 50 },
13 | pullRequests: { totalCount: 300 },
14 | issues: { totalCount: 200 },
15 | followers: { totalCount: 100 },
16 | repositories: {
17 | totalCount: 5,
18 | nodes: [
19 | { stargazers: { totalCount: 100 } },
20 | { stargazers: { totalCount: 100 } },
21 | { stargazers: { totalCount: 100 } },
22 | { stargazers: { totalCount: 50 } },
23 | { stargazers: { totalCount: 50 } },
24 | ],
25 | },
26 | },
27 | },
28 | };
29 |
30 | const error = {
31 | errors: [
32 | {
33 | type: "NOT_FOUND",
34 | path: ["user"],
35 | locations: [],
36 | message: "Could not resolve to a User with the login of 'noname'.",
37 | },
38 | ],
39 | };
40 |
41 | const mock = new MockAdapter(axios);
42 |
43 | afterEach(() => {
44 | mock.reset();
45 | });
46 |
47 | describe("Test fetchStats", () => {
48 | it("should fetch correct stats", async () => {
49 | mock.onPost("https://api.github.com/graphql").reply(200, data);
50 |
51 | let stats = await fetchStats("anuraghazra");
52 | const rank = calculateRank({
53 | totalCommits: 100,
54 | totalRepos: 5,
55 | followers: 100,
56 | contributions: 61,
57 | stargazers: 400,
58 | prs: 300,
59 | issues: 200,
60 | });
61 |
62 | expect(stats).toStrictEqual({
63 | contributedTo: 61,
64 | name: "Anurag Hazra",
65 | totalCommits: 100,
66 | totalIssues: 200,
67 | totalPRs: 300,
68 | totalStars: 400,
69 | rank,
70 | });
71 | });
72 |
73 | it("should throw error", async () => {
74 | mock.onPost("https://api.github.com/graphql").reply(200, error);
75 |
76 | await expect(fetchStats("anuraghazra")).rejects.toThrow(
77 | "Could not resolve to a User with the login of 'noname'."
78 | );
79 | });
80 |
81 | it("should fetch and add private contributions", async () => {
82 | mock.onPost("https://api.github.com/graphql").reply(200, data);
83 |
84 | let stats = await fetchStats("anuraghazra", true);
85 | const rank = calculateRank({
86 | totalCommits: 150,
87 | totalRepos: 5,
88 | followers: 100,
89 | contributions: 61,
90 | stargazers: 400,
91 | prs: 300,
92 | issues: 200,
93 | });
94 |
95 | expect(stats).toStrictEqual({
96 | contributedTo: 61,
97 | name: "Anurag Hazra",
98 | totalCommits: 150,
99 | totalIssues: 200,
100 | totalPRs: 300,
101 | totalStars: 400,
102 | rank,
103 | });
104 | });
105 | });
--------------------------------------------------------------------------------
/tests/fetchTopLanguages.test.js:
--------------------------------------------------------------------------------
1 | require("@testing-library/jest-dom");
2 | const axios = require("axios");
3 | const MockAdapter = require("axios-mock-adapter");
4 | const fetchTopLanguages = require("../src/fetchTopLanguages");
5 |
6 | const mock = new MockAdapter(axios);
7 |
8 | afterEach(() => {
9 | mock.reset();
10 | });
11 |
12 | const data_langs = {
13 | data: {
14 | user: {
15 | repositories: {
16 | nodes: [
17 | {
18 | languages: {
19 | edges: [{ size: 100, node: { color: "#0f0", name: "HTML" } }],
20 | },
21 | },
22 | {
23 | languages: {
24 | edges: [{ size: 100, node: { color: "#0f0", name: "HTML" } }],
25 | },
26 | },
27 | {
28 | languages: {
29 | edges: [
30 | { size: 100, node: { color: "#0ff", name: "javascript" } },
31 | ],
32 | },
33 | },
34 | {
35 | languages: {
36 | edges: [
37 | { size: 100, node: { color: "#0ff", name: "javascript" } },
38 | ],
39 | },
40 | },
41 | ],
42 | },
43 | },
44 | },
45 | };
46 |
47 | const error = {
48 | errors: [
49 | {
50 | type: "NOT_FOUND",
51 | path: ["user"],
52 | locations: [],
53 | message: "Could not resolve to a User with the login of 'noname'.",
54 | },
55 | ],
56 | };
57 |
58 | describe("FetchTopLanguages", () => {
59 | it("should fetch correct language data", async () => {
60 | mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
61 |
62 | let repo = await fetchTopLanguages("anuraghazra");
63 | expect(repo).toStrictEqual({
64 | HTML: {
65 | color: "#0f0",
66 | name: "HTML",
67 | size: 200,
68 | },
69 | javascript: {
70 | color: "#0ff",
71 | name: "javascript",
72 | size: 200,
73 | },
74 | });
75 | });
76 |
77 | it("should throw error", async () => {
78 | mock.onPost("https://api.github.com/graphql").reply(200, error);
79 |
80 | await expect(fetchTopLanguages("anuraghazra")).rejects.toThrow(
81 | "Could not resolve to a User with the login of 'noname'."
82 | );
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/tests/pin.test.js:
--------------------------------------------------------------------------------
1 | require("@testing-library/jest-dom");
2 | const axios = require("axios");
3 | const MockAdapter = require("axios-mock-adapter");
4 | const pin = require("../api/pin");
5 | const renderRepoCard = require("../src/renderRepoCard");
6 | const { renderError } = require("../src/utils");
7 |
8 | const data_repo = {
9 | repository: {
10 | username: "anuraghazra",
11 | name: "convoychat",
12 | stargazers: { totalCount: 38000 },
13 | description: "Help us take over the world! React + TS + GraphQL Chat App",
14 | primaryLanguage: {
15 | color: "#2b7489",
16 | id: "MDg6TGFuZ3VhZ2UyODc=",
17 | name: "TypeScript",
18 | },
19 | forkCount: 100,
20 | isTemplate: false
21 | },
22 | };
23 |
24 | const data_user = {
25 | data: {
26 | user: { repository: data_repo.repository },
27 | organization: null,
28 | },
29 | };
30 |
31 | const mock = new MockAdapter(axios);
32 |
33 | afterEach(() => {
34 | mock.reset();
35 | });
36 |
37 | describe("Test /api/pin", () => {
38 | it("should test the request", async () => {
39 | const req = {
40 | query: {
41 | username: "anuraghazra",
42 | repo: "convoychat",
43 | },
44 | };
45 | const res = {
46 | setHeader: jest.fn(),
47 | send: jest.fn(),
48 | };
49 | mock.onPost("https://api.github.com/graphql").reply(200, data_user);
50 |
51 | await pin(req, res);
52 |
53 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
54 | expect(res.send).toBeCalledWith(renderRepoCard(data_repo.repository));
55 | });
56 |
57 | it("should get the query options", async () => {
58 | const req = {
59 | query: {
60 | username: "anuraghazra",
61 | repo: "convoychat",
62 | title_color: "fff",
63 | icon_color: "fff",
64 | text_color: "fff",
65 | bg_color: "fff",
66 | full_name: "1",
67 | },
68 | };
69 | const res = {
70 | setHeader: jest.fn(),
71 | send: jest.fn(),
72 | };
73 | mock.onPost("https://api.github.com/graphql").reply(200, data_user);
74 |
75 | await pin(req, res);
76 |
77 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
78 | expect(res.send).toBeCalledWith(
79 | renderRepoCard(data_repo.repository, { ...req.query })
80 | );
81 | });
82 |
83 | it("should render error card if user repo not found", async () => {
84 | const req = {
85 | query: {
86 | username: "anuraghazra",
87 | repo: "convoychat",
88 | },
89 | };
90 | const res = {
91 | setHeader: jest.fn(),
92 | send: jest.fn(),
93 | };
94 | mock
95 | .onPost("https://api.github.com/graphql")
96 | .reply(200, { data: { user: { repository: null }, organization: null } });
97 |
98 | await pin(req, res);
99 |
100 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
101 | expect(res.send).toBeCalledWith(renderError("User Repository Not found"));
102 | });
103 |
104 | it("should render error card if org repo not found", async () => {
105 | const req = {
106 | query: {
107 | username: "anuraghazra",
108 | repo: "convoychat",
109 | },
110 | };
111 | const res = {
112 | setHeader: jest.fn(),
113 | send: jest.fn(),
114 | };
115 | mock
116 | .onPost("https://api.github.com/graphql")
117 | .reply(200, { data: { user: null, organization: { repository: null } } });
118 |
119 | await pin(req, res);
120 |
121 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
122 | expect(res.send).toBeCalledWith(
123 | renderError("Organization Repository Not found")
124 | );
125 | });
126 | });
127 |
--------------------------------------------------------------------------------
/tests/renderRepoCard.test.js:
--------------------------------------------------------------------------------
1 | require("@testing-library/jest-dom");
2 | const cssToObject = require("css-to-object");
3 | const renderRepoCard = require("../src/renderRepoCard");
4 |
5 | const { queryByTestId } = require("@testing-library/dom");
6 | const themes = require("../themes");
7 |
8 | const data_repo = {
9 | repository: {
10 | nameWithOwner: "anuraghazra/convoychat",
11 | name: "convoychat",
12 | stargazers: { totalCount: 38000 },
13 | description: "Help us take over the world! React + TS + GraphQL Chat App",
14 | primaryLanguage: {
15 | color: "#2b7489",
16 | id: "MDg6TGFuZ3VhZ2UyODc=",
17 | name: "TypeScript",
18 | },
19 | forkCount: 100,
20 | },
21 | };
22 |
23 | describe("Test renderRepoCard", () => {
24 | it("should render correctly", () => {
25 | document.body.innerHTML = renderRepoCard(data_repo.repository);
26 |
27 | const [header] = document.getElementsByClassName("header");
28 |
29 | expect(header).toHaveTextContent("convoychat");
30 | expect(header).not.toHaveTextContent("anuraghazra");
31 | expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
32 | "Help us take over the world! React + TS + GraphQL Chat App"
33 | );
34 | expect(queryByTestId(document.body, "stargazers")).toHaveTextContent("38k");
35 | expect(queryByTestId(document.body, "forkcount")).toHaveTextContent("100");
36 | expect(queryByTestId(document.body, "lang-name")).toHaveTextContent(
37 | "TypeScript"
38 | );
39 | expect(queryByTestId(document.body, "lang-color")).toHaveAttribute(
40 | "fill",
41 | "#2b7489"
42 | );
43 | });
44 |
45 | it("should display username in title (full repo name)", () => {
46 | document.body.innerHTML = renderRepoCard(data_repo.repository, {
47 | show_owner: true,
48 | });
49 | expect(document.getElementsByClassName("header")[0]).toHaveTextContent(
50 | "anuraghazra/convoychat"
51 | );
52 | });
53 |
54 | it("should trim description", () => {
55 | document.body.innerHTML = renderRepoCard({
56 | ...data_repo.repository,
57 | description:
58 | "The quick brown fox jumps over the lazy dog is an English-language pangram—a sentence that contains all of the letters of the English alphabet",
59 | });
60 |
61 | expect(
62 | document.getElementsByClassName("description")[0].children[0].textContent
63 | ).toBe("The quick brown fox jumps over the lazy dog is an");
64 |
65 | expect(
66 | document.getElementsByClassName("description")[0].children[1].textContent
67 | ).toBe("English-language pangram—a sentence that contains all");
68 |
69 | // Should not trim
70 | document.body.innerHTML = renderRepoCard({
71 | ...data_repo.repository,
72 | description: "Small text should not trim",
73 | });
74 |
75 | expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
76 | "Small text should not trim"
77 | );
78 | });
79 |
80 | it("should render emojis", () => {
81 | document.body.innerHTML = renderRepoCard({
82 | ...data_repo.repository,
83 | description: "This is a text with a :poop: poo emoji",
84 | });
85 |
86 | // poop emoji may not show in all editors but it's there between "a" and "poo"
87 | expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
88 | "This is a text with a 💩 poo emoji"
89 | );
90 | });
91 |
92 | it("should shift the text position depending on language length", () => {
93 | document.body.innerHTML = renderRepoCard({
94 | ...data_repo.repository,
95 | primaryLanguage: {
96 | ...data_repo.repository.primaryLanguage,
97 | name: "Jupyter Notebook",
98 | },
99 | });
100 |
101 | expect(queryByTestId(document.body, "primary-lang")).toBeInTheDocument();
102 | expect(queryByTestId(document.body, "star-fork-group")).toHaveAttribute(
103 | "transform",
104 | "translate(155, 0)"
105 | );
106 |
107 | // Small lang
108 | document.body.innerHTML = renderRepoCard({
109 | ...data_repo.repository,
110 | primaryLanguage: {
111 | ...data_repo.repository.primaryLanguage,
112 | name: "Ruby",
113 | },
114 | });
115 |
116 | expect(queryByTestId(document.body, "star-fork-group")).toHaveAttribute(
117 | "transform",
118 | "translate(125, 0)"
119 | );
120 | });
121 |
122 | it("should hide language if primaryLanguage is null & fallback to correct values", () => {
123 | document.body.innerHTML = renderRepoCard({
124 | ...data_repo.repository,
125 | primaryLanguage: null,
126 | });
127 |
128 | expect(queryByTestId(document.body, "primary-lang")).toBeNull();
129 |
130 | document.body.innerHTML = renderRepoCard({
131 | ...data_repo.repository,
132 | primaryLanguage: { color: null, name: null },
133 | });
134 |
135 | expect(queryByTestId(document.body, "primary-lang")).toBeInTheDocument();
136 | expect(queryByTestId(document.body, "lang-color")).toHaveAttribute(
137 | "fill",
138 | "#333"
139 | );
140 |
141 | expect(queryByTestId(document.body, "lang-name")).toHaveTextContent(
142 | "Unspecified"
143 | );
144 | });
145 |
146 | it("should render default colors properly", () => {
147 | document.body.innerHTML = renderRepoCard(data_repo.repository);
148 |
149 | const styleTag = document.querySelector("style");
150 | const stylesObject = cssToObject(styleTag.innerHTML);
151 |
152 | const headerClassStyles = stylesObject[".header"];
153 | const descClassStyles = stylesObject[".description"];
154 | const iconClassStyles = stylesObject[".icon"];
155 |
156 | expect(headerClassStyles.fill).toBe("#2f80ed");
157 | expect(descClassStyles.fill).toBe("#333");
158 | expect(iconClassStyles.fill).toBe("#586069");
159 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
160 | "fill",
161 | "#fffefe"
162 | );
163 | });
164 |
165 | it("should render custom colors properly", () => {
166 | const customColors = {
167 | title_color: "5a0",
168 | icon_color: "1b998b",
169 | text_color: "9991",
170 | bg_color: "252525",
171 | };
172 |
173 | document.body.innerHTML = renderRepoCard(data_repo.repository, {
174 | ...customColors,
175 | });
176 |
177 | const styleTag = document.querySelector("style");
178 | const stylesObject = cssToObject(styleTag.innerHTML);
179 |
180 | const headerClassStyles = stylesObject[".header"];
181 | const descClassStyles = stylesObject[".description"];
182 | const iconClassStyles = stylesObject[".icon"];
183 |
184 | expect(headerClassStyles.fill).toBe(`#${customColors.title_color}`);
185 | expect(descClassStyles.fill).toBe(`#${customColors.text_color}`);
186 | expect(iconClassStyles.fill).toBe(`#${customColors.icon_color}`);
187 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
188 | "fill",
189 | "#252525"
190 | );
191 | });
192 |
193 | it("should render with all the themes", () => {
194 | Object.keys(themes).forEach((name) => {
195 | document.body.innerHTML = renderRepoCard(data_repo.repository, {
196 | theme: name,
197 | });
198 |
199 | const styleTag = document.querySelector("style");
200 | const stylesObject = cssToObject(styleTag.innerHTML);
201 |
202 | const headerClassStyles = stylesObject[".header"];
203 | const descClassStyles = stylesObject[".description"];
204 | const iconClassStyles = stylesObject[".icon"];
205 |
206 | expect(headerClassStyles.fill).toBe(`#${themes[name].title_color}`);
207 | expect(descClassStyles.fill).toBe(`#${themes[name].text_color}`);
208 | expect(iconClassStyles.fill).toBe(`#${themes[name].icon_color}`);
209 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
210 | "fill",
211 | `#${themes[name].bg_color}`
212 | );
213 | });
214 | });
215 |
216 | it("should render custom colors with themes", () => {
217 | document.body.innerHTML = renderRepoCard(data_repo.repository, {
218 | title_color: "5a0",
219 | theme: "radical",
220 | });
221 |
222 | const styleTag = document.querySelector("style");
223 | const stylesObject = cssToObject(styleTag.innerHTML);
224 |
225 | const headerClassStyles = stylesObject[".header"];
226 | const descClassStyles = stylesObject[".description"];
227 | const iconClassStyles = stylesObject[".icon"];
228 |
229 | expect(headerClassStyles.fill).toBe("#5a0");
230 | expect(descClassStyles.fill).toBe(`#${themes.radical.text_color}`);
231 | expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`);
232 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
233 | "fill",
234 | `#${themes.radical.bg_color}`
235 | );
236 | });
237 |
238 | it("should render custom colors with themes and fallback to default colors if invalid", () => {
239 | document.body.innerHTML = renderRepoCard(data_repo.repository, {
240 | title_color: "invalid color",
241 | text_color: "invalid color",
242 | theme: "radical",
243 | });
244 |
245 | const styleTag = document.querySelector("style");
246 | const stylesObject = cssToObject(styleTag.innerHTML);
247 |
248 | const headerClassStyles = stylesObject[".header"];
249 | const descClassStyles = stylesObject[".description"];
250 | const iconClassStyles = stylesObject[".icon"];
251 |
252 | expect(headerClassStyles.fill).toBe(`#${themes.default.title_color}`);
253 | expect(descClassStyles.fill).toBe(`#${themes.default.text_color}`);
254 | expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`);
255 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
256 | "fill",
257 | `#${themes.radical.bg_color}`
258 | );
259 | });
260 |
261 | it("should not render star count or fork count if either of the are zero", () => {
262 | document.body.innerHTML = renderRepoCard({
263 | ...data_repo.repository,
264 | stargazers: { totalCount: 0 },
265 | });
266 |
267 | expect(queryByTestId(document.body, "stargazers")).toBeNull();
268 | expect(queryByTestId(document.body, "forkcount")).toBeInTheDocument();
269 |
270 | document.body.innerHTML = renderRepoCard({
271 | ...data_repo.repository,
272 | stargazers: { totalCount: 1 },
273 | forkCount: 0,
274 | });
275 |
276 | expect(queryByTestId(document.body, "stargazers")).toBeInTheDocument();
277 | expect(queryByTestId(document.body, "forkcount")).toBeNull();
278 |
279 | document.body.innerHTML = renderRepoCard({
280 | ...data_repo.repository,
281 | stargazers: { totalCount: 0 },
282 | forkCount: 0,
283 | });
284 |
285 | expect(queryByTestId(document.body, "stargazers")).toBeNull();
286 | expect(queryByTestId(document.body, "forkcount")).toBeNull();
287 | });
288 |
289 | it("should render badges", () => {
290 | document.body.innerHTML = renderRepoCard({
291 | ...data_repo.repository,
292 | isArchived: true,
293 | });
294 |
295 | expect(queryByTestId(document.body, "badge")).toHaveTextContent("Archived");
296 |
297 | document.body.innerHTML = renderRepoCard({
298 | ...data_repo.repository,
299 | isTemplate: true,
300 | });
301 | expect(queryByTestId(document.body, "badge")).toHaveTextContent("Template");
302 | });
303 |
304 | it("should not render template", () => {
305 | document.body.innerHTML = renderRepoCard({
306 | ...data_repo.repository,
307 | });
308 | expect(queryByTestId(document.body, "badge")).toBeNull();
309 | });
310 | });
311 |
--------------------------------------------------------------------------------
/tests/renderStatsCard.test.js:
--------------------------------------------------------------------------------
1 | require("@testing-library/jest-dom");
2 | const cssToObject = require("css-to-object");
3 | const renderStatsCard = require("../src/renderStatsCard");
4 |
5 | const {
6 | getByTestId,
7 | queryByTestId,
8 | queryAllByTestId,
9 | } = require("@testing-library/dom");
10 | const themes = require("../themes");
11 |
12 | describe("Test renderStatsCard", () => {
13 | const stats = {
14 | name: "Anurag Hazra",
15 | totalStars: 100,
16 | totalCommits: 200,
17 | totalIssues: 300,
18 | totalPRs: 400,
19 | contributedTo: 500,
20 | rank: { level: "A+", score: 40 },
21 | };
22 |
23 | it("should render correctly", () => {
24 | document.body.innerHTML = renderStatsCard(stats);
25 |
26 | expect(document.getElementsByClassName("header")[0].textContent).toBe(
27 | "Anurag Hazra's GitHub Stats"
28 | );
29 |
30 | expect(
31 | document.body.getElementsByTagName("svg")[0].getAttribute("height")
32 | ).toBe("195");
33 | expect(getByTestId(document.body, "stars").textContent).toBe("100");
34 | expect(getByTestId(document.body, "commits").textContent).toBe("200");
35 | expect(getByTestId(document.body, "issues").textContent).toBe("300");
36 | expect(getByTestId(document.body, "prs").textContent).toBe("400");
37 | expect(getByTestId(document.body, "contribs").textContent).toBe("500");
38 | expect(queryByTestId(document.body, "card-bg")).toBeInTheDocument();
39 | expect(queryByTestId(document.body, "rank-circle")).toBeInTheDocument();
40 | });
41 |
42 | it("should have proper name apostrophe", () => {
43 | document.body.innerHTML = renderStatsCard({ ...stats, name: "Anil Das" });
44 |
45 | expect(document.getElementsByClassName("header")[0].textContent).toBe(
46 | "Anil Das' GitHub Stats"
47 | );
48 |
49 | document.body.innerHTML = renderStatsCard({ ...stats, name: "Felix" });
50 |
51 | expect(document.getElementsByClassName("header")[0].textContent).toBe(
52 | "Felix' GitHub Stats"
53 | );
54 | });
55 |
56 | it("should hide individual stats", () => {
57 | document.body.innerHTML = renderStatsCard(stats, {
58 | hide: ["issues", "prs", "contribs"],
59 | });
60 |
61 | expect(
62 | document.body.getElementsByTagName("svg")[0].getAttribute("height")
63 | ).toBe("150"); // height should be 150 because we clamped it.
64 |
65 | expect(queryByTestId(document.body, "stars")).toBeDefined();
66 | expect(queryByTestId(document.body, "commits")).toBeDefined();
67 | expect(queryByTestId(document.body, "issues")).toBeNull();
68 | expect(queryByTestId(document.body, "prs")).toBeNull();
69 | expect(queryByTestId(document.body, "contribs")).toBeNull();
70 | });
71 |
72 | it("should hide_border", () => {
73 | document.body.innerHTML = renderStatsCard(stats, { hide_border: true });
74 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
75 | "stroke-opacity",
76 | "0"
77 | );
78 |
79 | document.body.innerHTML = renderStatsCard(stats, { hide_border: false });
80 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
81 | "stroke-opacity",
82 | "1"
83 | );
84 | });
85 |
86 | it("should hide_rank", () => {
87 | document.body.innerHTML = renderStatsCard(stats, { hide_rank: true });
88 |
89 | expect(queryByTestId(document.body, "rank-circle")).not.toBeInTheDocument();
90 | });
91 |
92 | it("should render default colors properly", () => {
93 | document.body.innerHTML = renderStatsCard(stats);
94 |
95 | const styleTag = document.querySelector("style");
96 | const stylesObject = cssToObject(styleTag.textContent);
97 |
98 | const headerClassStyles = stylesObject[".header"];
99 | const statClassStyles = stylesObject[".stat"];
100 | const iconClassStyles = stylesObject[".icon"];
101 |
102 | expect(headerClassStyles.fill).toBe("#2f80ed");
103 | expect(statClassStyles.fill).toBe("#333");
104 | expect(iconClassStyles.fill).toBe("#4c71f2");
105 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
106 | "fill",
107 | "#fffefe"
108 | );
109 | });
110 |
111 | it("should render custom colors properly", () => {
112 | const customColors = {
113 | title_color: "5a0",
114 | icon_color: "1b998b",
115 | text_color: "9991",
116 | bg_color: "252525",
117 | };
118 |
119 | document.body.innerHTML = renderStatsCard(stats, { ...customColors });
120 |
121 | const styleTag = document.querySelector("style");
122 | const stylesObject = cssToObject(styleTag.innerHTML);
123 |
124 | const headerClassStyles = stylesObject[".header"];
125 | const statClassStyles = stylesObject[".stat"];
126 | const iconClassStyles = stylesObject[".icon"];
127 |
128 | expect(headerClassStyles.fill).toBe(`#${customColors.title_color}`);
129 | expect(statClassStyles.fill).toBe(`#${customColors.text_color}`);
130 | expect(iconClassStyles.fill).toBe(`#${customColors.icon_color}`);
131 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
132 | "fill",
133 | "#252525"
134 | );
135 | });
136 |
137 | it("should render custom colors with themes", () => {
138 | document.body.innerHTML = renderStatsCard(stats, {
139 | title_color: "5a0",
140 | theme: "radical",
141 | });
142 |
143 | const styleTag = document.querySelector("style");
144 | const stylesObject = cssToObject(styleTag.innerHTML);
145 |
146 | const headerClassStyles = stylesObject[".header"];
147 | const statClassStyles = stylesObject[".stat"];
148 | const iconClassStyles = stylesObject[".icon"];
149 |
150 | expect(headerClassStyles.fill).toBe("#5a0");
151 | expect(statClassStyles.fill).toBe(`#${themes.radical.text_color}`);
152 | expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`);
153 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
154 | "fill",
155 | `#${themes.radical.bg_color}`
156 | );
157 | });
158 |
159 | it("should render with all the themes", () => {
160 | Object.keys(themes).forEach((name) => {
161 | document.body.innerHTML = renderStatsCard(stats, {
162 | theme: name,
163 | });
164 |
165 | const styleTag = document.querySelector("style");
166 | const stylesObject = cssToObject(styleTag.innerHTML);
167 |
168 | const headerClassStyles = stylesObject[".header"];
169 | const statClassStyles = stylesObject[".stat"];
170 | const iconClassStyles = stylesObject[".icon"];
171 |
172 | expect(headerClassStyles.fill).toBe(`#${themes[name].title_color}`);
173 | expect(statClassStyles.fill).toBe(`#${themes[name].text_color}`);
174 | expect(iconClassStyles.fill).toBe(`#${themes[name].icon_color}`);
175 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
176 | "fill",
177 | `#${themes[name].bg_color}`
178 | );
179 | });
180 | });
181 |
182 | it("should render custom colors with themes and fallback to default colors if invalid", () => {
183 | document.body.innerHTML = renderStatsCard(stats, {
184 | title_color: "invalid color",
185 | text_color: "invalid color",
186 | theme: "radical",
187 | });
188 |
189 | const styleTag = document.querySelector("style");
190 | const stylesObject = cssToObject(styleTag.innerHTML);
191 |
192 | const headerClassStyles = stylesObject[".header"];
193 | const statClassStyles = stylesObject[".stat"];
194 | const iconClassStyles = stylesObject[".icon"];
195 |
196 | expect(headerClassStyles.fill).toBe(`#${themes.default.title_color}`);
197 | expect(statClassStyles.fill).toBe(`#${themes.default.text_color}`);
198 | expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`);
199 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
200 | "fill",
201 | `#${themes.radical.bg_color}`
202 | );
203 | });
204 |
205 | it("should hide the title", () => {
206 | document.body.innerHTML = renderStatsCard(stats, {
207 | hide_title: true,
208 | });
209 |
210 | expect(document.getElementsByClassName("header")[0]).toBeUndefined();
211 | expect(document.getElementsByTagName("svg")[0]).toHaveAttribute(
212 | "height",
213 | "165"
214 | );
215 | expect(queryByTestId(document.body, "card-body-content")).toHaveAttribute(
216 | "transform",
217 | "translate(0, -30)"
218 | );
219 | });
220 |
221 | it("should not hide the title", () => {
222 | document.body.innerHTML = renderStatsCard(stats, {});
223 |
224 | expect(document.getElementsByClassName("header")[0]).toBeDefined();
225 | expect(document.getElementsByTagName("svg")[0]).toHaveAttribute(
226 | "height",
227 | "195"
228 | );
229 | expect(queryByTestId(document.body, "card-body-content")).toHaveAttribute(
230 | "transform",
231 | "translate(0, 0)"
232 | );
233 | });
234 |
235 | it("should render icons correctly", () => {
236 | document.body.innerHTML = renderStatsCard(stats, {
237 | show_icons: true,
238 | });
239 |
240 | expect(queryAllByTestId(document.body, "icon")[0]).toBeDefined();
241 | expect(queryByTestId(document.body, "stars")).toBeDefined();
242 | expect(
243 | queryByTestId(document.body, "stars").previousElementSibling // the label
244 | ).toHaveAttribute("x", "25");
245 | });
246 |
247 | it("should not have icons if show_icons is false", () => {
248 | document.body.innerHTML = renderStatsCard(stats, { show_icons: false });
249 |
250 | expect(queryAllByTestId(document.body, "icon")[0]).not.toBeDefined();
251 | expect(queryByTestId(document.body, "stars")).toBeDefined();
252 | expect(
253 | queryByTestId(document.body, "stars").previousElementSibling // the label
254 | ).not.toHaveAttribute("x");
255 | });
256 | });
257 |
--------------------------------------------------------------------------------
/tests/renderTopLanguages.test.js:
--------------------------------------------------------------------------------
1 | require("@testing-library/jest-dom");
2 | const cssToObject = require("css-to-object");
3 | const renderTopLanguages = require("../src/renderTopLanguages");
4 |
5 | const {
6 | getByTestId,
7 | queryByTestId,
8 | queryAllByTestId,
9 | } = require("@testing-library/dom");
10 | const themes = require("../themes");
11 |
12 | describe("Test renderTopLanguages", () => {
13 | const langs = {
14 | HTML: {
15 | color: "#0f0",
16 | name: "HTML",
17 | size: 200,
18 | },
19 | javascript: {
20 | color: "#0ff",
21 | name: "javascript",
22 | size: 200,
23 | },
24 | css: {
25 | color: "#ff0",
26 | name: "css",
27 | size: 100,
28 | },
29 | };
30 |
31 | it("should render correctly", () => {
32 | document.body.innerHTML = renderTopLanguages(langs);
33 |
34 | expect(queryByTestId(document.body, "header")).toHaveTextContent(
35 | "Most Used Languages"
36 | );
37 |
38 | expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent(
39 | "HTML"
40 | );
41 | expect(queryAllByTestId(document.body, "lang-name")[1]).toHaveTextContent(
42 | "javascript"
43 | );
44 | expect(queryAllByTestId(document.body, "lang-name")[2]).toHaveTextContent(
45 | "css"
46 | );
47 | expect(queryAllByTestId(document.body, "lang-progress")[0]).toHaveAttribute(
48 | "width",
49 | "40%"
50 | );
51 | expect(queryAllByTestId(document.body, "lang-progress")[1]).toHaveAttribute(
52 | "width",
53 | "40%"
54 | );
55 | expect(queryAllByTestId(document.body, "lang-progress")[2]).toHaveAttribute(
56 | "width",
57 | "20%"
58 | );
59 | });
60 |
61 | it("should hide languages when hide is passed", () => {
62 | document.body.innerHTML = renderTopLanguages(langs, {
63 | hide: ["HTML"],
64 | });
65 | expect(queryAllByTestId(document.body, "lang-name")[0]).toBeInTheDocument(
66 | "javascript"
67 | );
68 | expect(queryAllByTestId(document.body, "lang-name")[1]).toBeInTheDocument(
69 | "css"
70 | );
71 | expect(queryAllByTestId(document.body, "lang-name")[2]).not.toBeDefined();
72 |
73 | // multiple languages passed
74 | document.body.innerHTML = renderTopLanguages(langs, {
75 | hide: ["HTML","css"],
76 | });
77 | expect(queryAllByTestId(document.body, "lang-name")[0]).toBeInTheDocument(
78 | "javascript"
79 | );
80 | expect(queryAllByTestId(document.body, "lang-name")[1]).not.toBeDefined();
81 | });
82 |
83 | it("should resize the height correctly depending on langs", () => {
84 | document.body.innerHTML = renderTopLanguages(langs, {});
85 | expect(document.querySelector("svg")).toHaveAttribute("height", "205");
86 |
87 | document.body.innerHTML = renderTopLanguages(
88 | {
89 | ...langs,
90 | python: {
91 | color: "#ff0",
92 | name: "python",
93 | size: 100,
94 | },
95 | },
96 | {}
97 | );
98 | expect(document.querySelector("svg")).toHaveAttribute("height", "245");
99 | });
100 |
101 | it("should hide_title", () => {
102 | document.body.innerHTML = renderTopLanguages(langs, { hide_title: false });
103 | expect(document.querySelector("svg")).toHaveAttribute("height", "205");
104 | expect(queryByTestId(document.body, "lang-items")).toHaveAttribute(
105 | "y",
106 | "55"
107 | );
108 |
109 | // Lets hide now
110 | document.body.innerHTML = renderTopLanguages(langs, { hide_title: true });
111 | expect(document.querySelector("svg")).toHaveAttribute("height", "175");
112 |
113 | expect(queryByTestId(document.body, "header")).not.toBeInTheDocument();
114 | expect(queryByTestId(document.body, "lang-items")).toHaveAttribute(
115 | "y",
116 | "25"
117 | );
118 | });
119 |
120 | it("should render with custom width set", () => {
121 | document.body.innerHTML = renderTopLanguages(langs, {});
122 |
123 | expect(document.querySelector("svg")).toHaveAttribute("width", "300");
124 |
125 | document.body.innerHTML = renderTopLanguages(langs, { card_width: 400 });
126 | expect(document.querySelector("svg")).toHaveAttribute("width", "400");
127 | });
128 |
129 | it("should render default colors properly", () => {
130 | document.body.innerHTML = renderTopLanguages(langs);
131 |
132 | const styleTag = document.querySelector("style");
133 | const stylesObject = cssToObject(styleTag.textContent);
134 |
135 | const headerStyles = stylesObject[".header"];
136 | const langNameStyles = stylesObject[".lang-name"];
137 |
138 | expect(headerStyles.fill).toBe("#2f80ed");
139 | expect(langNameStyles.fill).toBe("#333");
140 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
141 | "fill",
142 | "#fffefe"
143 | );
144 | });
145 |
146 | it("should render custom colors properly", () => {
147 | const customColors = {
148 | title_color: "5a0",
149 | icon_color: "1b998b",
150 | text_color: "9991",
151 | bg_color: "252525",
152 | };
153 |
154 | document.body.innerHTML = renderTopLanguages(langs, { ...customColors });
155 |
156 | const styleTag = document.querySelector("style");
157 | const stylesObject = cssToObject(styleTag.innerHTML);
158 |
159 | const headerStyles = stylesObject[".header"];
160 | const langNameStyles = stylesObject[".lang-name"];
161 |
162 | expect(headerStyles.fill).toBe(`#${customColors.title_color}`);
163 | expect(langNameStyles.fill).toBe(`#${customColors.text_color}`);
164 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
165 | "fill",
166 | "#252525"
167 | );
168 | });
169 |
170 | it("should render custom colors with themes", () => {
171 | document.body.innerHTML = renderTopLanguages(langs, {
172 | title_color: "5a0",
173 | theme: "radical",
174 | });
175 |
176 | const styleTag = document.querySelector("style");
177 | const stylesObject = cssToObject(styleTag.innerHTML);
178 |
179 | const headerStyles = stylesObject[".header"];
180 | const langNameStyles = stylesObject[".lang-name"];
181 |
182 | expect(headerStyles.fill).toBe("#5a0");
183 | expect(langNameStyles.fill).toBe(`#${themes.radical.text_color}`);
184 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
185 | "fill",
186 | `#${themes.radical.bg_color}`
187 | );
188 | });
189 |
190 | it("should render with all the themes", () => {
191 | Object.keys(themes).forEach((name) => {
192 | document.body.innerHTML = renderTopLanguages(langs, {
193 | theme: name,
194 | });
195 |
196 | const styleTag = document.querySelector("style");
197 | const stylesObject = cssToObject(styleTag.innerHTML);
198 |
199 | const headerStyles = stylesObject[".header"];
200 | const langNameStyles = stylesObject[".lang-name"];
201 |
202 | expect(headerStyles.fill).toBe(`#${themes[name].title_color}`);
203 | expect(langNameStyles.fill).toBe(`#${themes[name].text_color}`);
204 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
205 | "fill",
206 | `#${themes[name].bg_color}`
207 | );
208 | });
209 | });
210 |
211 | it('should render with layout compact', () => {
212 | document.body.innerHTML = renderTopLanguages(langs, {layout: 'compact'});
213 |
214 | expect(queryByTestId(document.body, "header")).toHaveTextContent("Most Used Languages");
215 |
216 | expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent("HTML 40.00%");
217 | expect(queryAllByTestId(document.body, "lang-progress")[0]).toHaveAttribute("width","120.00");
218 |
219 | expect(queryAllByTestId(document.body, "lang-name")[1]).toHaveTextContent("javascript 40.00%");
220 | expect(queryAllByTestId(document.body, "lang-progress")[1]).toHaveAttribute("width","120.00");
221 |
222 | expect(queryAllByTestId(document.body, "lang-name")[2]).toHaveTextContent("css 20.00%");
223 | expect(queryAllByTestId(document.body, "lang-progress")[2]).toHaveAttribute("width","60.00");
224 | })
225 | });
226 |
--------------------------------------------------------------------------------
/tests/retryer.test.js:
--------------------------------------------------------------------------------
1 | require("@testing-library/jest-dom");
2 | const retryer = require("../src/retryer");
3 | const { logger } = require("../src/utils");
4 |
5 | const fetcher = jest.fn((variables, token) => {
6 | logger.log(variables, token);
7 | return new Promise((res, rej) => res({ data: "ok" }));
8 | });
9 |
10 | const fetcherFail = jest.fn(() => {
11 | return new Promise((res, rej) =>
12 | res({ data: { errors: [{ type: "RATE_LIMITED" }] } })
13 | );
14 | });
15 |
16 | const fetcherFailOnSecondTry = jest.fn((_vars, _token, retries) => {
17 | return new Promise((res, rej) => {
18 | // faking rate limit
19 | if (retries < 1) {
20 | return res({ data: { errors: [{ type: "RATE_LIMITED" }] } });
21 | }
22 | return res({ data: "ok" });
23 | });
24 | });
25 |
26 | describe("Test Retryer", () => {
27 | it("retryer should return value and have zero retries on first try", async () => {
28 | let res = await retryer(fetcher, {});
29 |
30 | expect(fetcher).toBeCalledTimes(1);
31 | expect(res).toStrictEqual({ data: "ok" });
32 | });
33 |
34 | it("retryer should return value and have 2 retries", async () => {
35 | let res = await retryer(fetcherFailOnSecondTry, {});
36 |
37 | expect(fetcherFailOnSecondTry).toBeCalledTimes(2);
38 | expect(res).toStrictEqual({ data: "ok" });
39 | });
40 |
41 | it("retryer should throw error if maximum retries reached", async () => {
42 | let res;
43 |
44 | try {
45 | res = await retryer(fetcherFail, {});
46 | } catch (err) {
47 | expect(fetcherFail).toBeCalledTimes(8);
48 | expect(err.message).toBe("Maximum retries exceeded");
49 | }
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/tests/top-langs.test.js:
--------------------------------------------------------------------------------
1 | require("@testing-library/jest-dom");
2 | const axios = require("axios");
3 | const MockAdapter = require("axios-mock-adapter");
4 | const topLangs = require("../api/top-langs");
5 | const renderTopLanguages = require("../src/renderTopLanguages");
6 | const { renderError } = require("../src/utils");
7 |
8 | const data_langs = {
9 | data: {
10 | user: {
11 | repositories: {
12 | nodes: [
13 | {
14 | languages: {
15 | edges: [{ size: 150, node: { color: "#0f0", name: "HTML" } }],
16 | },
17 | },
18 | {
19 | languages: {
20 | edges: [{ size: 100, node: { color: "#0f0", name: "HTML" } }],
21 | },
22 | },
23 | {
24 | languages: {
25 | edges: [
26 | { size: 100, node: { color: "#0ff", name: "javascript" } },
27 | ],
28 | },
29 | },
30 | {
31 | languages: {
32 | edges: [
33 | { size: 100, node: { color: "#0ff", name: "javascript" } },
34 | ],
35 | },
36 | },
37 | ],
38 | },
39 | },
40 | },
41 | };
42 |
43 | const error = {
44 | errors: [
45 | {
46 | type: "NOT_FOUND",
47 | path: ["user"],
48 | locations: [],
49 | message: "Could not fetch user",
50 | },
51 | ],
52 | };
53 |
54 | const langs = {
55 | HTML: {
56 | color: "#0f0",
57 | name: "HTML",
58 | size: 250,
59 | },
60 | javascript: {
61 | color: "#0ff",
62 | name: "javascript",
63 | size: 200,
64 | },
65 | };
66 |
67 | const mock = new MockAdapter(axios);
68 |
69 | afterEach(() => {
70 | mock.reset();
71 | });
72 |
73 | describe("Test /api/top-langs", () => {
74 | it("should test the request", async () => {
75 | const req = {
76 | query: {
77 | username: "anuraghazra",
78 | },
79 | };
80 | const res = {
81 | setHeader: jest.fn(),
82 | send: jest.fn(),
83 | };
84 | mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
85 |
86 | await topLangs(req, res);
87 |
88 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
89 | expect(res.send).toBeCalledWith(renderTopLanguages(langs));
90 | });
91 |
92 | it("should work with the query options", async () => {
93 | const req = {
94 | query: {
95 | username: "anuraghazra",
96 | hide_title: true,
97 | card_width: 100,
98 | title_color: "fff",
99 | icon_color: "fff",
100 | text_color: "fff",
101 | bg_color: "fff",
102 | },
103 | };
104 | const res = {
105 | setHeader: jest.fn(),
106 | send: jest.fn(),
107 | };
108 | mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
109 |
110 | await topLangs(req, res);
111 |
112 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
113 | expect(res.send).toBeCalledWith(
114 | renderTopLanguages(langs, {
115 | hide_title: true,
116 | card_width: 100,
117 | title_color: "fff",
118 | icon_color: "fff",
119 | text_color: "fff",
120 | bg_color: "fff",
121 | })
122 | );
123 | });
124 |
125 | it("should render error card on error", async () => {
126 | const req = {
127 | query: {
128 | username: "anuraghazra",
129 | },
130 | };
131 | const res = {
132 | setHeader: jest.fn(),
133 | send: jest.fn(),
134 | };
135 | mock.onPost("https://api.github.com/graphql").reply(200, error);
136 |
137 | await topLangs(req, res);
138 |
139 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
140 | expect(res.send).toBeCalledWith(renderError(error.errors[0].message));
141 | });
142 | });
143 |
--------------------------------------------------------------------------------
/tests/utils.test.js:
--------------------------------------------------------------------------------
1 | require("@testing-library/jest-dom");
2 | const {
3 | kFormatter,
4 | encodeHTML,
5 | renderError,
6 | FlexLayout,
7 | getCardColors,
8 | wrapTextMultiline,
9 | } = require("../src/utils");
10 |
11 | const { queryByTestId } = require("@testing-library/dom");
12 |
13 | describe("Test utils.js", () => {
14 | it("should test kFormatter", () => {
15 | expect(kFormatter(1)).toBe(1);
16 | expect(kFormatter(-1)).toBe(-1);
17 | expect(kFormatter(500)).toBe(500);
18 | expect(kFormatter(1000)).toBe("1k");
19 | expect(kFormatter(10000)).toBe("10k");
20 | expect(kFormatter(12345)).toBe("12.3k");
21 | expect(kFormatter(9900000)).toBe("9900k");
22 | });
23 |
24 | it("should test encodeHTML", () => {
25 | expect(encodeHTML(`hello world<,.#4^&^@%!))`)).toBe(
26 | "<html>hello world<,.#4^&^@%!))"
27 | );
28 | });
29 |
30 | it("should test renderError", () => {
31 | document.body.innerHTML = renderError("Something went wrong");
32 | expect(
33 | queryByTestId(document.body, "message").children[0]
34 | ).toHaveTextContent(/Something went wrong/gim);
35 | expect(queryByTestId(document.body, "message").children[1]).toBeEmpty(2);
36 |
37 | // Secondary message
38 | document.body.innerHTML = renderError(
39 | "Something went wrong",
40 | "Secondary Message"
41 | );
42 | expect(
43 | queryByTestId(document.body, "message").children[1]
44 | ).toHaveTextContent(/Secondary Message/gim);
45 | });
46 |
47 | it("should test FlexLayout", () => {
48 | const layout = FlexLayout({
49 | items: ["1 ", "2 "],
50 | gap: 60,
51 | }).join("");
52 |
53 | expect(layout).toBe(
54 | `1 2 `
55 | );
56 |
57 | const columns = FlexLayout({
58 | items: ["1 ", "2 "],
59 | gap: 60,
60 | direction: "column",
61 | }).join("");
62 |
63 | expect(columns).toBe(
64 | `1 2 `
65 | );
66 | });
67 |
68 | it("getCardColors: should return expected values", () => {
69 | let colors = getCardColors({
70 | title_color: "f00",
71 | text_color: "0f0",
72 | icon_color: "00f",
73 | bg_color: "fff",
74 | theme: "dark",
75 | });
76 | expect(colors).toStrictEqual({
77 | titleColor: "#f00",
78 | textColor: "#0f0",
79 | iconColor: "#00f",
80 | bgColor: "#fff",
81 | });
82 | });
83 |
84 | it("getCardColors: should fallback to default colors if color is invalid", () => {
85 | let colors = getCardColors({
86 | title_color: "invalidcolor",
87 | text_color: "0f0",
88 | icon_color: "00f",
89 | bg_color: "fff",
90 | theme: "dark",
91 | });
92 | expect(colors).toStrictEqual({
93 | titleColor: "#2f80ed",
94 | textColor: "#0f0",
95 | iconColor: "#00f",
96 | bgColor: "#fff",
97 | });
98 | });
99 |
100 | it("getCardColors: should fallback to specified theme colors if is not defined", () => {
101 | let colors = getCardColors({
102 | theme: "dark",
103 | });
104 | expect(colors).toStrictEqual({
105 | titleColor: "#fff",
106 | textColor: "#9f9f9f",
107 | iconColor: "#79ff97",
108 | bgColor: "#151515",
109 | });
110 | });
111 | });
112 |
113 | describe("wrapTextMultiline", () => {
114 | it("should not wrap small texts", () => {
115 | {
116 | let multiLineText = wrapTextMultiline("Small text should not wrap");
117 | expect(multiLineText).toEqual(["Small text should not wrap"]);
118 | }
119 | });
120 | it("should wrap large texts", () => {
121 | let multiLineText = wrapTextMultiline(
122 | "Hello world long long long text",
123 | 20,
124 | 3
125 | );
126 | expect(multiLineText).toEqual(["Hello world long", "long long text"]);
127 | });
128 | it("should wrap large texts and limit max lines", () => {
129 | let multiLineText = wrapTextMultiline(
130 | "Hello world long long long text",
131 | 10,
132 | 2
133 | );
134 | expect(multiLineText).toEqual(["Hello", "world long..."]);
135 | });
136 | });
137 |
--------------------------------------------------------------------------------
/themes/README.md:
--------------------------------------------------------------------------------
1 | ## Available Themes
2 |
3 |
4 |
5 | With inbuilt themes you can customize the look of the card without doing any manual customization.
6 |
7 | Use `?theme=THEME_NAME` parameter like so :-
8 |
9 | ```md
10 | 
11 | ```
12 |
13 | ## Stats
14 |
15 | > These themes work both for the Stats Card and Repo Card.
16 |
17 | | | | |
18 | | :--: | :--: | :--: |
19 | | `default` ![default][default] | `dark` ![dark][dark] | `radical` ![radical][radical] |
20 | | `merko` ![merko][merko] | `gruvbox` ![gruvbox][gruvbox] | `tokyonight` ![tokyonight][tokyonight] |
21 | | `onedark` ![onedark][onedark] | `cobalt` ![cobalt][cobalt] | `synthwave` ![synthwave][synthwave] |
22 | | `highcontrast` ![highcontrast][highcontrast] | `dracula` ![dracula][dracula] | `prussian` ![prussian][prussian] |
23 | | `monokai` ![monokai][monokai] | `vue` ![vue][vue] | `shades-of-purple` ![shades-of-purple][shades-of-purple] |
24 | | `nightowl` ![nightowl][nightowl] | `buefy` ![buefy][buefy] | `blue-green` ![blue-green][blue-green] |
25 | | `algolia` ![algolia][algolia] | `great-gatsby` ![great-gatsby][great-gatsby] | [Add your theme][add-theme] |
26 |
27 | ## Repo Card
28 |
29 | > These themes work both for the Stats Card and Repo Card.
30 |
31 | | | | |
32 | | :--: | :--: | :--: |
33 | | `default_repocard` ![default_repocard][default_repocard_repo] | `dark` ![dark][dark_repo] | `radical` ![radical][radical_repo] |
34 | | `merko` ![merko][merko_repo] | `gruvbox` ![gruvbox][gruvbox_repo] | `tokyonight` ![tokyonight][tokyonight_repo] |
35 | | `onedark` ![onedark][onedark_repo] | `cobalt` ![cobalt][cobalt_repo] | `synthwave` ![synthwave][synthwave_repo] |
36 | | `highcontrast` ![highcontrast][highcontrast_repo] | `dracula` ![dracula][dracula_repo] | `prussian` ![prussian][prussian_repo] |
37 | | `monokai` ![monokai][monokai_repo] | `vue` ![vue][vue_repo] | `shades-of-purple` ![shades-of-purple][shades-of-purple_repo] |
38 | | `nightowl` ![nightowl][nightowl_repo] | `buefy` ![buefy][buefy_repo] | `blue-green` ![blue-green][blue-green_repo] |
39 | | `algolia` ![algolia][algolia_repo] | `great-gatsby` ![great-gatsby][great-gatsby_repo] | [Add your theme][add-theme] |
40 |
41 |
42 | [default]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=default
43 | [default_repocard]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=default_repocard
44 | [dark]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=dark
45 | [radical]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=radical
46 | [merko]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=merko
47 | [gruvbox]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=gruvbox
48 | [tokyonight]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=tokyonight
49 | [onedark]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=onedark
50 | [cobalt]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=cobalt
51 | [synthwave]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=synthwave
52 | [highcontrast]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=highcontrast
53 | [dracula]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=dracula
54 | [prussian]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=prussian
55 | [monokai]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=monokai
56 | [vue]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=vue
57 | [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
58 | [nightowl]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=nightowl
59 | [buefy]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=buefy
60 | [blue-green]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=blue-green
61 | [algolia]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=algolia
62 | [great-gatsby]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=great-gatsby
63 |
64 |
65 | [default_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=default
66 | [default_repocard_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=default_repocard
67 | [dark_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=dark
68 | [radical_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=radical
69 | [merko_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=merko
70 | [gruvbox_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=gruvbox
71 | [tokyonight_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=tokyonight
72 | [onedark_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=onedark
73 | [cobalt_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=cobalt
74 | [synthwave_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=synthwave
75 | [highcontrast_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=highcontrast
76 | [dracula_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=dracula
77 | [prussian_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=prussian
78 | [monokai_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=monokai
79 | [vue_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=vue
80 | [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
81 | [nightowl_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=nightowl
82 | [buefy_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=buefy
83 | [blue-green_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=blue-green
84 | [algolia_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=algolia
85 | [great-gatsby_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=great-gatsby
86 |
87 |
88 | [add-theme]: https://github.com/anuraghazra/github-readme-stats/edit/master/themes/index.js
89 |
90 | Wanted to add a new theme? Consider reading the [contribution guidelines](../CONTRIBUTING.md#themes-contribution) :D
91 |
--------------------------------------------------------------------------------
/themes/index.js:
--------------------------------------------------------------------------------
1 | const themes = {
2 | default: {
3 | title_color: "2f80ed",
4 | icon_color: "4c71f2",
5 | text_color: "333",
6 | bg_color: "fffefe",
7 | },
8 | default_repocard: {
9 | title_color: "2f80ed",
10 | icon_color: "586069", // icon color is different
11 | text_color: "333",
12 | bg_color: "fffefe",
13 | },
14 | dark: {
15 | title_color: "fff",
16 | icon_color: "79ff97",
17 | text_color: "9f9f9f",
18 | bg_color: "151515",
19 | },
20 | radical: {
21 | title_color: "fe428e",
22 | icon_color: "f8d847",
23 | text_color: "a9fef7",
24 | bg_color: "141321",
25 | },
26 | merko: {
27 | title_color: "abd200",
28 | icon_color: "b7d364",
29 | text_color: "68b587",
30 | bg_color: "0a0f0b",
31 | },
32 | gruvbox: {
33 | title_color: "fabd2f",
34 | icon_color: "fe8019",
35 | text_color: "8ec07c",
36 | bg_color: "282828",
37 | },
38 | tokyonight: {
39 | title_color: "70a5fd",
40 | icon_color: "bf91f3",
41 | text_color: "38bdae",
42 | bg_color: "1a1b27",
43 | },
44 | onedark: {
45 | title_color: "e4bf7a",
46 | icon_color: "8eb573",
47 | text_color: "df6d74",
48 | bg_color: "282c34",
49 | },
50 | cobalt: {
51 | title_color: "e683d9",
52 | icon_color: "0480ef",
53 | text_color: "75eeb2",
54 | bg_color: "193549",
55 | },
56 | synthwave: {
57 | title_color: "e2e9ec",
58 | icon_color: "ef8539",
59 | text_color: "e5289e",
60 | bg_color: "2b213a",
61 | },
62 | highcontrast: {
63 | title_color: "e7f216",
64 | icon_color: "00ffff",
65 | text_color: "fff",
66 | bg_color: "000",
67 | },
68 | dracula: {
69 | title_color: "ff6e96",
70 | icon_color: "79dafa",
71 | text_color: "f8f8f2",
72 | bg_color: "282a36",
73 | },
74 | prussian: {
75 | title_color: "bddfff",
76 | icon_color: "38a0ff",
77 | text_color: "6e93b5",
78 | bg_color: "172f45",
79 | },
80 | monokai: {
81 | title_color: "eb1f6a",
82 | icon_color: "e28905",
83 | text_color: "f1f1eb",
84 | bg_color: "272822",
85 | },
86 | vue: {
87 | title_color: "41b883",
88 | icon_color: "41b883",
89 | text_color: "273849",
90 | bg_color: "fffefe",
91 | },
92 | "shades-of-purple": {
93 | title_color: "fad000",
94 | icon_color: "b362ff",
95 | text_color: "a599e9",
96 | bg_color: "2d2b55",
97 | },
98 | nightowl: {
99 | title_color: "c792ea",
100 | icon_color: "ffeb95",
101 | text_color: "7fdbca",
102 | bg_color: "011627",
103 | },
104 | buefy: {
105 | title_color: "7957d5",
106 | icon_color: "ff3860",
107 | text_color: "363636",
108 | bg_color: "ffffff",
109 | },
110 | "blue-green": {
111 | title_color: "2f97c1",
112 | icon_color: "f5b700",
113 | text_color: "0cf574",
114 | bg_color: "040f0f",
115 | },
116 | "algolia": {
117 | title_color: "00AEFF",
118 | icon_color: "2DDE98",
119 | text_color: "FFFFFF",
120 | bg_color: "050F2C",
121 | },
122 | "great-gatsby":{
123 | title_color: "ffa726",
124 | icon_color: "ffb74d",
125 | text_color: "ffd95b",
126 | bg_color: "000000",
127 | },
128 | };
129 |
130 | module.exports = themes;
131 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "redirects": [
3 | {
4 | "source": "/",
5 | "destination": "https://github.com/anuraghazra/github-readme-stats"
6 | }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------