├── .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 | GitHub Readme Stats 3 |

GitHub Readme Stats

4 |

Get dynamically generated GitHub stats on your readmes!

5 |

6 |

7 | 8 | Tests Passing 9 | 10 | 11 | 12 | 13 | 14 | Issues 15 | 16 | 17 | GitHub pull requests 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 | [![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra)](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 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=contribs,prs) 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 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&count_private=true) 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 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true) 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 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=radical) 107 | ``` 108 | 109 | #### All inbuilt themes :- 110 | 111 | dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula 112 | 113 | GitHub Readme Stat Themes 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 | [![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](https://github.com/anuraghazra/github-readme-stats) 157 | ``` 158 | 159 | ### Demo 160 | 161 | [![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](https://github.com/anuraghazra/github-readme-stats) 162 | 163 | Use [show_owner](#customization) variable to include the repo's owner username 164 | 165 | [![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&show_owner=true)](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 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](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 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&hide=javascript,html)](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 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=compact)](https://github.com/anuraghazra/github-readme-stats) 201 | ``` 202 | 203 | ### Demo 204 | 205 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats) 206 | 207 | - Compact layout 208 | 209 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=compact)](https://github.com/anuraghazra/github-readme-stats) 210 | 211 | --- 212 | 213 | ### All Demos 214 | 215 | - Default 216 | 217 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra) 218 | 219 | - Hiding specific stats 220 | 221 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=contribs,issues) 222 | 223 | - Showing icons 224 | 225 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=issues&show_icons=true) 226 | 227 | - Themes 228 | 229 | Choose from any of the [default themes](#themes) 230 | 231 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=radical) 232 | 233 | - Customizing stats card 234 | 235 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api/?username=anuraghazra&show_icons=true&title_color=fff&icon_color=79ff97&text_color=9f9f9f&bg_color=151515) 236 | 237 | - Customizing repo card 238 | 239 | ![Customized Card](https://github-readme-stats.vercel.app/api/pin?username=anuraghazra&repo=github-readme-stats&title_color=fff&icon_color=f9f9f9&text_color=9f9f9f&bg_color=151515) 240 | 241 | - Top languages 242 | 243 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](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 | [![Deploy to Vercel](https://vercel.com/button)](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 | ![](https://files.catbox.moe/tct1wg.png) 274 | 1. Sign in with GitHub by pressing `Continue with GitHub` 275 | ![](https://files.catbox.moe/btd78j.jpeg) 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 | ![](https://files.catbox.moe/qckos0.png) 281 | 1. Select `Import Git Repository` 282 | ![](https://files.catbox.moe/pqub9q.png) 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 | ![](https://files.catbox.moe/0ez4g7.png) 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 | - [![paypal.me/anuraghazra](https://ionicabizau.github.io/badges/paypal.svg)](https://www.paypal.me/anuraghazra) - You can make one-time donations via PayPal. I'll probably buy a ~~coffee~~ tea. :tea: 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 | GitHub Readme Stats 3 |

GitHub Readme Stats

4 |

在你的 README 中获取动态生成的 GitHub 统计信息!

5 |

6 | 7 |

8 | 9 | Tests Passing 10 | 11 | 12 | 13 | 14 | 15 | Issues 16 | 17 | 18 | GitHub pull requests 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 | [![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra)](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 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=contribs,prs) 76 | ``` 77 | 78 | ### 把私人贡献计数添加到总提交计数中 79 | 80 | 你可以用参数 `?count_private=true` 把私人贡献计数添加到总提交计数中。 81 | 82 | _注:如果你是自己部署本项目,私人贡献将会默认被计数,如果不是自己部署,你需要分享你的私人贡献计数。_ 83 | 84 | > Options: `&count_private=true` 85 | 86 | ```md 87 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&count_private=true) 88 | ``` 89 | 90 | ### 显示图标 91 | 92 | 想要显示图标,你可以调用 `show_icons=true` 参数,如下: 93 | 94 | ```md 95 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true) 96 | ``` 97 | 98 | ### 主题 99 | 100 | 你可以通过现有的主题进行卡片个性化,省去[手动自定义](#自定义)的麻烦。 101 | 102 | 调用 `?theme=THEME_NAME` 参数,如下: 103 | 104 | ```md 105 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=radical) 106 | ``` 107 | 108 | #### 所有现有主题 109 | 110 | dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula 111 | 112 | GitHub Readme Stat Themes 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 | [![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](https://github.com/anuraghazra/github-readme-stats) 156 | ``` 157 | 158 | ### Demo 159 | 160 | [![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](https://github.com/anuraghazra/github-readme-stats) 161 | 162 | 使用 [show_owner](#自定义) 变量将 Repo 所属账户的用户名包含在内。 163 | 164 | [![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&show_owner=true)](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 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats) 180 | ``` 181 | 182 | ### 隐藏特定语言 183 | 184 | 可以使用`?hide=语言1,语言2`参数来隐藏指定的语言。 185 | 186 | ```md 187 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&hide=语言1,语言2)](https://github.com/anuraghazra/github-readme-stats) 188 | ``` 189 | 190 | ### 紧凑的语言卡片布局 191 | 192 | 你可以使用 `&layout=compact` 参数来改变卡片的样式。 193 | 194 | ```md 195 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=compact)](https://github.com/anuraghazra/github-readme-stats) 196 | ``` 197 | 198 | ### Demo 199 | 200 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats) 201 | 202 | - 紧凑布局 203 | 204 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=compact)](https://github.com/anuraghazra/github-readme-stats) 205 | 206 | --- 207 | 208 | ### 全部 Demo 209 | 210 | - 默认 211 | 212 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra) 213 | 214 | - 隐藏特定数据 215 | 216 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=contribs,issues) 217 | 218 | - 显示图标 219 | 220 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=issues&show_icons=true) 221 | 222 | - 主题 223 | 224 | 从 [默认主题](#主题) 中进行选择 225 | 226 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=radical) 227 | 228 | - 自定义统计卡片 229 | 230 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api/?username=anuraghazra&show_icons=true&title_color=fff&icon_color=79ff97&text_color=9f9f9f&bg_color=151515) 231 | 232 | - 自定义代码库卡片 233 | 234 | ![Customized Card](https://github-readme-stats.vercel.app/api/pin?username=anuraghazra&repo=github-readme-stats&title_color=fff&icon_color=f9f9f9&text_color=9f9f9f&bg_color=151515) 235 | 236 | - 热门语言 237 | 238 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](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 | [![Deploy to Vercel](https://vercel.com/button)](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 | ![](https://files.catbox.moe/tct1wg.png) 270 | 1. 点击 `Continue with GitHub` 通过 GitHub 进行登录 271 | ![](https://files.catbox.moe/btd78j.jpeg) 272 | 1. 登录 GitHub 并允许访问所有存储库(如果系统这样提示) 273 | 1. Fork 这个仓库 274 | 1. 返回到你的 [Vercel dashboard](https://vercel.com/dashboard) 275 | 1. 选择 `Import Project` 276 | ![](https://files.catbox.moe/qckos0.png) 277 | 1. 选择 `Import Git Repository` 278 | ![](https://files.catbox.moe/pqub9q.png) 279 | 1. 选择 root 并将所有内容保持不变,并且只需添加名为 PAT_1 的环境变量(如图所示),其中将包含一个个人访问令牌(PAT),你可以在[这里](https://github.com/settings/tokens/new)轻松创建(保留默认,并且只需要命名下,名字随便) 280 | ![](https://files.catbox.moe/caem5b.png) 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 | - [![paypal.me/anuraghazra](https://ionicabizau.github.io/badges/paypal.svg)](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 | GitHub Readme Stats 3 |

GitHub Readme Stats

4 |

Zeige dynamisch generierte GitHub-Statistiken in deinen Readmes!

5 |

6 | 7 |

8 | 9 | Tests Passing 10 | 11 | 12 | 13 | 14 | 15 | Issues 16 | 17 | 18 | GitHub pull requests 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 | [![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra)](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 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=["contribs","prs"]) 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 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true) 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 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=radical) 87 | ``` 88 | 89 | #### Alle eingebauten Themes :- 90 | 91 | dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula 92 | 93 | GitHub Readme Stat Themes 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 | [![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](https://github.com/anuraghazra/github-readme-stats) 136 | ``` 137 | 138 | ### Beispiele 139 | 140 | [![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](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 | [![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&show_owner=true)](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 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](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 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&hide_langs_below=1)](https://github.com/anuraghazra/github-readme-stats) 168 | ``` 169 | 170 | ### Beispiel 171 | 172 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats) 173 | 174 | --- 175 | 176 | ### Alle Beispiele 177 | 178 | - Default 179 | 180 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra) 181 | 182 | - Ausblenden bestimmter Statistiken 183 | 184 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=["contribs","issues"]) 185 | 186 | - Icons anzeigen 187 | 188 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=["issues"]&show_icons=true) 189 | 190 | - Erscheinungsbild/Themes 191 | 192 | Choose from any of the [default themes](#themes) 193 | 194 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=radical) 195 | 196 | - Stats Card anpassen 197 | 198 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api/?username=anuraghazra&show_icons=true&title_color=fff&icon_color=79ff97&text_color=9f9f9f&bg_color=151515) 199 | 200 | - Repo Card anpassen 201 | 202 | ![Customized Card](https://github-readme-stats.vercel.app/api/pin?username=anuraghazra&repo=github-readme-stats&title_color=fff&icon_color=f9f9f9&text_color=9f9f9f&bg_color=151515) 203 | 204 | - Top Programmiersprachen 205 | 206 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](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 | [![Deploy to Vercel](https://vercel.com/button)](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 | ![](https://files.catbox.moe/tct1wg.png) 238 | 1. Melde dich mit deinem GitHub-account an, indem du `Continue with GitHub` klickst 239 | ![](https://files.catbox.moe/btd78j.jpeg) 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 | ![](https://files.catbox.moe/qckos0.png) 245 | 1. Klick `Import Git Repository` 246 | ![](https://files.catbox.moe/pqub9q.png) 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 | ![](https://files.catbox.moe/0ez4g7.png) 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 | - [![paypal.me/anuraghazra](https://ionicabizau.github.io/badges/paypal.svg)](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 | GitHub Readme Stats 3 |

GitHub Readme Stats

4 |

¡Obtén tus estadísticas de GitHub generadas dinámicamente en tu README!

5 |

6 | 7 |

8 | 9 | Tests Passing 10 | 11 | 12 | Issues 13 | 14 | 15 | GitHub pull requests 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 | [![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra)](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 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=["contribs","prs"]) 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 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&count_private=true) 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 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true) 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 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=radical) 102 | ``` 103 | 104 | #### Todos los temas incorporados 105 | 106 | dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula 107 | 108 | GitHub Readme Stat Themes 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 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra) 139 | 140 | - Ocultando estadísticas específicas 141 | 142 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=["contribs","issues"]) 143 | 144 | - Mostrando íconos 145 | 146 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=["issues"]&show_icons=true) 147 | 148 | - Temas 149 | 150 | Elige uno de los [temas predeterminados](#temas) 151 | 152 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=radical) 153 | 154 | - Personalizando la tarjeta de estadísticas 155 | 156 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api/?username=anuraghazra&show_icons=true&title_color=fff&icon_color=79ff97&text_color=9f9f9f&bg_color=151515) 157 | 158 | - Personalizando la tarjeta de repositorio 159 | 160 | ![Customized Card](https://github-readme-stats.vercel.app/api/pin?username=anuraghazra&repo=github-readme-stats&title_color=fff&icon_color=f9f9f9&text_color=9f9f9f&bg_color=151515) 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 | [![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](https://github.com/anuraghazra/github-readme-stats) 178 | ``` 179 | 180 | ### Ejemplo 181 | 182 | [![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](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 | [![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&show_owner=true)](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 | [![Deploy to Vercel](https://vercel.com/button)](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 | ![](https://files.catbox.moe/tct1wg.png) 215 | 1. Inicia sesión con GitHub presionando `Continue with GitHub` 216 | ![](https://files.catbox.moe/btd78j.jpeg) 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 | ![](https://files.catbox.moe/qckos0.png) 222 | 1. Selecciona `Import Git Repository` 223 | ![](https://files.catbox.moe/pqub9q.png) 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 | ![](https://files.catbox.moe/caem5b.png) 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 | - [![paypal.me/anuraghazra](https://ionicabizau.github.io/badges/paypal.svg)](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 | GitHub Readme Stats 3 |

GitHub Readme Stats

4 |

あなたのREADMEに動的に生成されたGitHubの統計情報を載せましょう!

5 |

6 | 7 |

8 | 9 | Tests Passing 10 | 11 | 12 | 13 | 14 | 15 | Issues 16 | 17 | 18 | GitHub pull requests 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 | [![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra)](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 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=contribs,prs) 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 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&count_private=true) 89 | ``` 90 | 91 | ### Showing icons 92 | 93 | クエリパラメータ `?show_icons=true` を使用することで、アイコンが表示が有効になります。 94 | 95 | ```md 96 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true) 97 | ``` 98 | 99 | ### Themes 100 | 101 | 内蔵されているテーマを使用することで、任意の[手動のカスタマイズ](#customization)を行うことなく、カードの外観をカスタマイズすることができます。 102 | 103 | `?theme=THEME_NAME` は以下のように使います。 104 | 105 | ```md 106 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=radical) 107 | ``` 108 | 109 | #### All inbuilt themes :- 110 | 111 | dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula 112 | 113 | GitHub Readme Stat Themes 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 | [![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](https://github.com/anuraghazra/github-readme-stats) 157 | ``` 158 | 159 | ### Demo 160 | 161 | [![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](https://github.com/anuraghazra/github-readme-stats) 162 | 163 | リポジトリのオーナーのユーザー名を含める場合は、show_owner 変数を使用します。 164 | 165 | [![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&show_owner=true)](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 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats) 181 | ``` 182 | 183 | ### Hide individual languages 184 | 185 | クエリパラメータ `?hide=language1,language2` 使用することで、個々の言語を非表示にすることができます。 186 | 187 | ```md 188 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&hide=javascript,html)](https://github.com/anuraghazra/github-readme-stats) 189 | ``` 190 | 191 | ### Compact Language Card Layout 192 | 193 | クエリパラメータ `&layout=compact` を使用することで、カードのデザインを変更することができます。 194 | 195 | ```md 196 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=compact)](https://github.com/anuraghazra/github-readme-stats) 197 | ``` 198 | 199 | ### Demo 200 | 201 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats) 202 | 203 | - Compact layout 204 | 205 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=compact)](https://github.com/anuraghazra/github-readme-stats) 206 | 207 | --- 208 | 209 | ### All Demos 210 | 211 | - Default 212 | 213 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra) 214 | 215 | - Hiding specific stats 216 | 217 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=contribs,issues) 218 | 219 | - Showing icons 220 | 221 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=issues&show_icons=true) 222 | 223 | - Themes 224 | 225 | 任意の[テーマ](#themes)を選択できます。 226 | 227 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=radical) 228 | 229 | - Customizing stats card 230 | 231 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api/?username=anuraghazra&show_icons=true&title_color=fff&icon_color=79ff97&text_color=9f9f9f&bg_color=151515) 232 | 233 | - Customizing repo card 234 | 235 | ![Customized Card](https://github-readme-stats.vercel.app/api/pin?username=anuraghazra&repo=github-readme-stats&title_color=fff&icon_color=f9f9f9&text_color=9f9f9f&bg_color=151515) 236 | 237 | - Top languages 238 | 239 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](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 | [![Deploy to Vercel](https://vercel.com/button)](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 | ![](https://files.catbox.moe/tct1wg.png) 270 | 1. `Continue with GitHub` を押して GitHub にサインインします。 271 | ![](https://files.catbox.moe/btd78j.jpeg) 272 | 1. GitHub にサインインし、すべてのリポジトリへのアクセスを許可します。 273 | 1. このリポジトリをフォークします。 274 | 1. [Vercel dashboard](https://vercel.com/dashboard)に戻ります。 275 | 1. `Import Project` を選択します。 276 | ![](https://files.catbox.moe/qckos0.png) 277 | 1. `Import Git Repository` を選択します。 278 | ![](https://files.catbox.moe/pqub9q.png) 279 | 1. root を選択して、すべてをそのままにしておき、PAT_1 という名前の環境変数を(下図のように)追加します。これには個人アクセストークン (PAT) が含まれており、[ここ](https://github.com/settings/tokens/new)で簡単に作成することができます (すべてをそのままにしておいて、何かに名前を付けてください。) 280 | ![](https://files.catbox.moe/0ez4g7.png) 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 | - [![paypal.me/anuraghazra](https://ionicabizau.github.io/badges/paypal.svg)](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 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&theme=dark&show_icons=true) 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 | ${header} 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 | : `${encodeHTML(name)}'${apostrophe} GitHub Stats`; 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 | : `Most Used Languages` 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 | `12` 55 | ); 56 | 57 | const columns = FlexLayout({ 58 | items: ["1", "2"], 59 | gap: 60, 60 | direction: "column", 61 | }).join(""); 62 | 63 | expect(columns).toBe( 64 | `12` 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 | ![Anurag's github stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&theme=dark&show_icons=true) 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 | --------------------------------------------------------------------------------