├── .gitattributes
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── labeler.yml
├── stale.yml
└── workflows
│ ├── generate-theme-doc.yml
│ ├── label-pr.yml
│ ├── preview-theme.yml
│ └── test.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .vercelignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── api
├── index.js
├── pin.js
├── top-langs.js
└── wakatime.js
├── codecov.yml
├── docs
├── readme_cn.md
├── readme_de.md
├── readme_es.md
├── readme_fr.md
├── readme_it.md
├── readme_ja.md
├── readme_kr.md
├── readme_nl.md
├── readme_np.md
├── readme_pt-BR.md
└── readme_tr.md
├── jest.config.js
├── package.json
├── powered-by-vercel.svg
├── readme.md
├── scripts
├── generate-theme-doc.js
├── preview-theme.js
└── push-theme-readme.sh
├── src
├── calculateRank.js
├── cards
│ ├── repo-card.js
│ ├── stats-card.js
│ ├── top-languages-card.js
│ └── wakatime-card.js
├── common
│ ├── Card.js
│ ├── I18n.js
│ ├── blacklist.js
│ ├── createProgressNode.js
│ ├── icons.js
│ ├── languageColors.json
│ ├── retryer.js
│ └── utils.js
├── fetchers
│ ├── repo-fetcher.js
│ ├── stats-fetcher.js
│ ├── top-languages-fetcher.js
│ └── wakatime-fetcher.js
├── getStyles.js
└── translations.js
├── tests
├── __snapshots__
│ └── renderWakatimeCard.test.js.snap
├── api.test.js
├── calculateRank.test.js
├── card.test.js
├── fetchRepo.test.js
├── fetchStats.test.js
├── fetchTopLanguages.test.js
├── fetchWakatime.test.js
├── pin.test.js
├── renderRepoCard.test.js
├── renderStatsCard.test.js
├── renderTopLanguages.test.js
├── renderWakatimeCard.test.js
├── retryer.test.js
├── top-langs.test.js
└── utils.test.js
├── themes
├── README.md
└── index.js
└── vercel.json
/.gitattributes:
--------------------------------------------------------------------------------
1 | dist/* linguist-vendored=false
2 |
--------------------------------------------------------------------------------
/.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: [
13 | "https://www.paypal.me/anuraghazra",
14 | "https://www.buymeacoffee.com/anuraghazra",
15 | ] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
--------------------------------------------------------------------------------
/.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 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **Expected behavior**
13 | A clear and concise description of what you expected to happen.
14 |
15 | **Screenshots / Live demo link (paste the github-readme-stats link as markdown image)**
16 | If applicable, add screenshots to help explain your problem.
17 |
18 | **Additional context**
19 | Add any other context about the problem here.
20 |
21 |
42 |
--------------------------------------------------------------------------------
/.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 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
21 |
42 |
--------------------------------------------------------------------------------
/.github/labeler.yml:
--------------------------------------------------------------------------------
1 | themes: themes/index.js
2 | doc-translation: docs/*
3 | card-i18n: src/translations.js
4 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 30
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 7
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - feature
8 | - enhancement
9 | - help wanted
10 | - bug
11 |
12 | # Label to use when marking an issue as stale
13 | staleLabel: stale
14 | # Comment to post when marking an issue as stale. Set to `false` to disable
15 | markComment: >
16 | This issue has been automatically marked as stale because it has not had
17 | recent activity. It will be closed if no further activity occurs. Thank you
18 | for your contributions.
19 | # Comment to post when closing a stale issue. Set to `false` to disable
20 | closeComment: false
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/label-pr.yml:
--------------------------------------------------------------------------------
1 | name: "Pull Request Labeler"
2 | on:
3 | - pull_request_target
4 |
5 | jobs:
6 | triage:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/labeler@v2
10 | with:
11 | repo-token: "${{ secrets.GITHUB_TOKEN }}"
12 |
--------------------------------------------------------------------------------
/.github/workflows/preview-theme.yml:
--------------------------------------------------------------------------------
1 | name: Theme preview
2 |
3 | on:
4 | pull_request_target:
5 | types: [opened, synchronize, reopened]
6 | branches:
7 | - master
8 | - theme-preview-script
9 | - "themes/index.js"
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | name: Install & Preview
15 |
16 | steps:
17 | - uses: actions/checkout@v1
18 | - uses: bahmutov/npm-install@v1
19 | with:
20 | useLockFile: false
21 | - run: npm run preview-theme
22 | env:
23 | CI: true
24 | PERSONAL_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
25 |
--------------------------------------------------------------------------------
/.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:
30 | ${{ runner.os }}-npm-cache-${{ hashFiles('**/package-lock.json') }}
31 | restore-keys: |
32 | ${{ runner.os }}-npm-cache-
33 |
34 | - name: Install & Test
35 | run: |
36 | npm install
37 | npm run test
38 |
39 | - name: Code Coverage
40 | uses: codecov/codecov-action@v1
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vercel
2 | .env
3 | node_modules
4 | package-lock.json
5 | *.lock
6 | .vscode/
7 | .idea/
8 | coverage
9 | vercel_token
10 |
11 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.json
3 | *.md
4 | coverage
5 | .vercel
6 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all",
3 | "useTabs": false,
4 | "endOfLine": "auto",
5 | "proseWrap": "always"
6 | }
--------------------------------------------------------------------------------
/.vercelignore:
--------------------------------------------------------------------------------
1 | .env
2 | package-lock.json
3 | coverage
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at hazru.anurag@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/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_1" 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 | ## Frequently Asked Questions (FAQs)
51 |
52 | **Q:** How to hide jupyter Notebook?
53 |
54 | > **Ans:** &hide=jupyter%20notebook
55 |
56 | **Q:** I could not figure out how to deploy on my own vercel instance
57 |
58 | > **Ans:**
59 | >
60 | > - docs: https://github.com/anuraghazra/github-readme-stats/#deploy-on-your-own-vercel-instance
61 | > - YT tutorial by codeSTACKr: https://www.youtube.com/watch?v=n6d4KHSKqGk&feature=youtu.be&t=107
62 |
63 | **Q:** Language Card is incorrect
64 |
65 | > **Ans:** Please read all the related issues / comments before opening any issues regarding language card stats:
66 | >
67 | > - https://github.com/anuraghazra/github-readme-stats/issues/136#issuecomment-665164174
68 | >
69 | > - https://github.com/anuraghazra/github-readme-stats/issues/136#issuecomment-665172181
70 |
71 | **Q:** How to count private stats?
72 |
73 | > **Ans:** We can only count private commits & we cannot access any other private info of any users, so it's not possible. only way is to deploy on your own instance & use your own PAT (Personal Access Token)
74 |
75 | ### Bug Reports
76 |
77 | **Great Bug Reports** tend to have:
78 |
79 | - A quick summary and/or background
80 | - Steps to reproduce
81 | - Be specific!
82 | - Share the snapshot, if possible.
83 | - GitHub Readme Stats' live link
84 | - What actually happens
85 | - What you expected would happen
86 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
87 |
88 | People _love_ thorough bug reports. I'm not even kidding.
89 |
90 | ### Feature Request
91 |
92 | **Great Feature Requests** tend to have:
93 |
94 | - A quick idea summary
95 | - What & why you wanted to add the specific feature
96 | - Additional Context like images, links to resources to implement the feature etc etc.
97 |
98 | ## License
99 |
100 | By contributing, you agree that your contributions will be licensed under its [MIT License](./LICENSE).
101 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 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/common/utils");
9 | const fetchStats = require("../src/fetchers/stats-fetcher");
10 | const renderStatsCard = require("../src/cards/stats-card");
11 | const blacklist = require("../src/common/blacklist");
12 | const { isLocaleAvailable } = require("../src/translations");
13 |
14 | module.exports = async (req, res) => {
15 | const {
16 | username,
17 | hide,
18 | hide_title,
19 | hide_border,
20 | hide_rank,
21 | show_icons,
22 | count_private,
23 | include_all_commits,
24 | line_height,
25 | title_color,
26 | icon_color,
27 | text_color,
28 | bg_color,
29 | theme,
30 | cache_seconds,
31 | custom_title,
32 | locale,
33 | disable_animations,
34 | border_radius,
35 | } = req.query;
36 | let stats;
37 |
38 | res.setHeader("Content-Type", "image/svg+xml");
39 |
40 | if (blacklist.includes(username)) {
41 | return res.send(renderError("Something went wrong"));
42 | }
43 |
44 | if (locale && !isLocaleAvailable(locale)) {
45 | return res.send(renderError("Something went wrong", "Language not found"));
46 | }
47 |
48 | try {
49 | stats = await fetchStats(
50 | username,
51 | parseBoolean(count_private),
52 | parseBoolean(include_all_commits),
53 | );
54 |
55 | const cacheSeconds = clampValue(
56 | parseInt(cache_seconds || CONSTANTS.TWO_HOURS, 10),
57 | CONSTANTS.TWO_HOURS,
58 | CONSTANTS.ONE_DAY,
59 | );
60 |
61 | res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`);
62 |
63 | return res.send(
64 | renderStatsCard(stats, {
65 | hide: parseArray(hide),
66 | show_icons: parseBoolean(show_icons),
67 | hide_title: parseBoolean(hide_title),
68 | hide_border: parseBoolean(hide_border),
69 | hide_rank: parseBoolean(hide_rank),
70 | include_all_commits: parseBoolean(include_all_commits),
71 | line_height,
72 | title_color,
73 | icon_color,
74 | text_color,
75 | bg_color,
76 | theme,
77 | custom_title,
78 | border_radius,
79 | locale: locale ? locale.toLowerCase() : null,
80 | disable_animations: parseBoolean(disable_animations),
81 | }),
82 | );
83 | } catch (err) {
84 | return res.send(renderError(err.message, err.secondaryMessage));
85 | }
86 | };
87 |
--------------------------------------------------------------------------------
/api/pin.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const {
3 | renderError,
4 | parseBoolean,
5 | clampValue,
6 | CONSTANTS,
7 | } = require("../src/common/utils");
8 | const fetchRepo = require("../src/fetchers/repo-fetcher");
9 | const renderRepoCard = require("../src/cards/repo-card");
10 | const blacklist = require("../src/common/blacklist");
11 | const { isLocaleAvailable } = require("../src/translations");
12 |
13 | module.exports = async (req, res) => {
14 | const {
15 | username,
16 | repo,
17 | hide_border,
18 | title_color,
19 | icon_color,
20 | text_color,
21 | bg_color,
22 | theme,
23 | show_owner,
24 | cache_seconds,
25 | locale,
26 | border_radius,
27 | } = req.query;
28 |
29 | let repoData;
30 |
31 | res.setHeader("Content-Type", "image/svg+xml");
32 |
33 | if (blacklist.includes(username)) {
34 | return res.send(renderError("Something went wrong"));
35 | }
36 |
37 | if (locale && !isLocaleAvailable(locale)) {
38 | return res.send(renderError("Something went wrong", "Language not found"));
39 | }
40 |
41 | try {
42 | repoData = await fetchRepo(username, repo);
43 |
44 | let cacheSeconds = clampValue(
45 | parseInt(cache_seconds || CONSTANTS.TWO_HOURS, 10),
46 | CONSTANTS.TWO_HOURS,
47 | CONSTANTS.ONE_DAY,
48 | );
49 |
50 | /*
51 | if star count & fork count is over 1k then we are kFormating the text
52 | and if both are zero we are not showing the stats
53 | so we can just make the cache longer, since there is no need to frequent updates
54 | */
55 | const stars = repoData.stargazers.totalCount;
56 | const forks = repoData.forkCount;
57 | const isBothOver1K = stars > 1000 && forks > 1000;
58 | const isBothUnder1 = stars < 1 && forks < 1;
59 | if (!cache_seconds && (isBothOver1K || isBothUnder1)) {
60 | cacheSeconds = CONSTANTS.FOUR_HOURS;
61 | }
62 |
63 | res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`);
64 |
65 | return res.send(
66 | renderRepoCard(repoData, {
67 | hide_border,
68 | title_color,
69 | icon_color,
70 | text_color,
71 | bg_color,
72 | theme,
73 | border_radius,
74 | show_owner: parseBoolean(show_owner),
75 | locale: locale ? locale.toLowerCase() : null,
76 | }),
77 | );
78 | } catch (err) {
79 | return res.send(renderError(err.message, err.secondaryMessage));
80 | }
81 | };
82 |
--------------------------------------------------------------------------------
/api/top-langs.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const {
3 | renderError,
4 | clampValue,
5 | parseBoolean,
6 | parseArray,
7 | CONSTANTS,
8 | } = require("../src/common/utils");
9 | const fetchTopLanguages = require("../src/fetchers/top-languages-fetcher");
10 | const renderTopLanguages = require("../src/cards/top-languages-card");
11 | const blacklist = require("../src/common/blacklist");
12 | const { isLocaleAvailable } = require("../src/translations");
13 |
14 | module.exports = async (req, res) => {
15 | const {
16 | username,
17 | hide,
18 | hide_title,
19 | hide_border,
20 | card_width,
21 | title_color,
22 | text_color,
23 | bg_color,
24 | theme,
25 | cache_seconds,
26 | layout,
27 | langs_count,
28 | exclude_repo,
29 | custom_title,
30 | locale,
31 | border_radius
32 | } = req.query;
33 | let topLangs;
34 |
35 | res.setHeader("Content-Type", "image/svg+xml");
36 |
37 | if (blacklist.includes(username)) {
38 | return res.send(renderError("Something went wrong"));
39 | }
40 |
41 | if (locale && !isLocaleAvailable(locale)) {
42 | return res.send(renderError("Something went wrong", "Language not found"));
43 | }
44 |
45 | try {
46 | topLangs = await fetchTopLanguages(
47 | username,
48 | langs_count,
49 | parseArray(exclude_repo),
50 | );
51 |
52 | const cacheSeconds = clampValue(
53 | parseInt(cache_seconds || CONSTANTS.TWO_HOURS, 10),
54 | CONSTANTS.TWO_HOURS,
55 | CONSTANTS.ONE_DAY,
56 | );
57 |
58 | res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`);
59 |
60 | return res.send(
61 | renderTopLanguages(topLangs, {
62 | custom_title,
63 | hide_title: parseBoolean(hide_title),
64 | hide_border: parseBoolean(hide_border),
65 | card_width: parseInt(card_width, 10),
66 | hide: parseArray(hide),
67 | title_color,
68 | text_color,
69 | bg_color,
70 | theme,
71 | layout,
72 | border_radius,
73 | locale: locale ? locale.toLowerCase() : null,
74 | }),
75 | );
76 | } catch (err) {
77 | return res.send(renderError(err.message, err.secondaryMessage));
78 | }
79 | };
80 |
--------------------------------------------------------------------------------
/api/wakatime.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const {
3 | renderError,
4 | parseBoolean,
5 | clampValue,
6 | CONSTANTS,
7 | isLocaleAvailable,
8 | } = require("../src/common/utils");
9 | const { fetchWakatimeStats } = require("../src/fetchers/wakatime-fetcher");
10 | const wakatimeCard = require("../src/cards/wakatime-card");
11 |
12 | module.exports = async (req, res) => {
13 | const {
14 | username,
15 | title_color,
16 | icon_color,
17 | hide_border,
18 | line_height,
19 | text_color,
20 | bg_color,
21 | theme,
22 | cache_seconds,
23 | hide_title,
24 | hide_progress,
25 | custom_title,
26 | locale,
27 | layout,
28 | api_domain,
29 | range,
30 | border_radius,
31 | } = req.query;
32 |
33 | res.setHeader("Content-Type", "image/svg+xml");
34 |
35 | if (locale && !isLocaleAvailable(locale)) {
36 | return res.send(renderError("Something went wrong", "Language not found"));
37 | }
38 |
39 | try {
40 | const stats = await fetchWakatimeStats({ username, api_domain, range });
41 |
42 | let cacheSeconds = clampValue(
43 | parseInt(cache_seconds || CONSTANTS.TWO_HOURS, 10),
44 | CONSTANTS.TWO_HOURS,
45 | CONSTANTS.ONE_DAY,
46 | );
47 |
48 | if (!cache_seconds) {
49 | cacheSeconds = CONSTANTS.FOUR_HOURS;
50 | }
51 |
52 | res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`);
53 |
54 | return res.send(
55 | wakatimeCard(stats, {
56 | custom_title,
57 | hide_title: parseBoolean(hide_title),
58 | hide_border: parseBoolean(hide_border),
59 | line_height,
60 | title_color,
61 | icon_color,
62 | text_color,
63 | bg_color,
64 | theme,
65 | hide_progress,
66 | border_radius,
67 | locale: locale ? locale.toLowerCase() : null,
68 | layout,
69 | }),
70 | );
71 | } catch (err) {
72 | return res.send(renderError(err.message, err.secondaryMessage));
73 | }
74 | };
75 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs/readme_cn.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
GitHub Readme Stats
4 | 在你的 README 中获取动态生成的 GitHub 统计信息!
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | 查看 Demo
31 | ·
32 | 报告 Bug
33 | ·
34 | 请求增加功能
35 |
36 |
37 | Français
38 | ·
39 | 简体中文
40 | ·
41 | Español
42 | ·
43 | Deutsch
44 | ·
45 | 日本語
46 | ·
47 | Português Brasileiro
48 | ·
49 | Italiano
50 | ·
51 | 한국어
52 | .
53 | Nederlands
54 | .
55 | नेपाली
56 |
57 |
58 | 喜欢这个项目?请考虑捐赠来帮助它完善!
59 |
60 | # 特性
61 |
62 | - [GitHub 统计卡片](#GitHub-统计卡片)
63 | - [GitHub 更多置顶](#GitHub-更多置顶)
64 | - [热门语言卡片](#热门语言卡片)
65 | - [主题](#主题)
66 | - [自定义](#自定义)
67 | - [自己部署](#自己部署)
68 |
69 | # GitHub 统计卡片
70 |
71 | 将这行代码复制到你的 markdown 文件中,就是如此简单!
72 |
73 | 更改 `?username=` 的值为你的 GitHub 用户名。
74 |
75 | ```md
76 | [](https://github.com/anuraghazra/github-readme-stats)
77 | ```
78 |
79 | _注: 等级基于用户的统计信息计算得出,详见 [src/calculateRank.js](../src/calculateRank.js)_
80 |
81 | ### 隐藏指定统计
82 |
83 | 想要隐藏指定统计信息,你可以调用参数 `?hide=`,其值用 `,` 分隔。
84 |
85 | > 选项:`&hide=stars,commits,prs,issues,contribs`
86 |
87 | ```md
88 | 
89 | ```
90 |
91 | ### 将私人项目贡献添加到总提交计数中
92 |
93 | 你可以使用参数 `?count_private=true` 把私人贡献计数添加到总提交计数中。
94 |
95 | _注:如果你是自己部署本项目,私人贡献将会默认被计数,如果不是自己部署,你需要分享你的私人贡献计数。_
96 |
97 | > 选项: `&count_private=true`
98 |
99 | ```md
100 | 
101 | ```
102 |
103 | ### 显示图标
104 |
105 | 如果想要显示图标,你可以调用 `show_icons=true` 参数,像这样:
106 |
107 | ```md
108 | 
109 | ```
110 |
111 | ### 主题
112 |
113 | 你可以通过现有的主题进行卡片个性化,省去[手动自定义](#自定义)的麻烦。
114 |
115 | 通过调用 `?theme=THEME_NAME` 参数,像这样:
116 |
117 | ```md
118 | 
119 | ```
120 |
121 | #### 所有现有主题
122 |
123 | dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula
124 |
125 |
126 |
127 | 你可以预览[所有可用主题](../themes/README.md)或者签出[主题配置文件](../themes/index.js), 而且如果你喜欢, **你也可以贡献新的主题** :D
128 |
129 | ### 自定义
130 |
131 | 你可以通过使用 URL 参数的方式,为你的 `Stats Card` 或 `Repo Card` 自定义样式。
132 |
133 | 常用选项:
134 |
135 | - `title_color` - 卡片标题颜色 _(十六进制色码)_
136 | - `text_color` - 内容文本颜色 _(十六进制色码)_
137 | - `icon_color` - 图标颜色(如果可用)_(十六进制色码)_
138 | - `bg_color` - 卡片背景颜色 _(十六进制色码)_ **或者** 以 _angle,start,end_ 的形式渐变
139 | - `hide_border` - 隐藏卡的边框 _(布尔值)_
140 | - `theme` - 主题名称,从[所有可用主题](../themes/README.md)中选择
141 | - `cache_seconds` - 手动设置缓存头 _(最小值: 1800,最大值: 86400)_
142 | - `locale` - 在卡片中设置语言 _(例如 cn, de, es, 等等)_
143 |
144 | ##### bg_color 渐变
145 |
146 | 你可以在 bg_color 选项中提供多个逗号分隔的值来呈现渐变,渐变的格式是 :-
147 |
148 | ```
149 | &bg_color=DEG,COLOR1,COLOR2,COLOR3...COLOR10
150 | ```
151 |
152 | > 缓存的注意事项: 如果 fork 数和 star 数 少于 1k , Repo 卡片默认缓存是 4 小时 (14400 秒) ,否则是 2 小时(7200)。另请注意缓存被限制为最短 2 小时,最长 24 小时。
153 |
154 | #### 统计卡片专属选项:
155 |
156 | - `hide` - 隐藏特定统计信息 _(以逗号分隔)_
157 | - `hide_title` - _(boolean)_
158 | - `hide_rank` - _(boolean)_
159 | - `show_icons` - _(boolean)_
160 | - `include_all_commits` - 统计总提交次数而不是仅统计今年的提交次数 _(boolean)_
161 | - `count_private` - 统计私人提交 _(boolean)_
162 | - `line_height` - 设置文本之间的行高 _(number)_
163 |
164 | #### Repo 卡片专属选项:
165 |
166 | - `show_owner` - 显示 Repo 的所有者名字 _(boolean)_
167 |
168 | #### 语言卡片专属选项:
169 |
170 | - `hide` - 从卡片中隐藏指定语言 _(Comma seperated values)_
171 | - `hide_title` - _(boolean)_
172 | - `layout` - 在两个可用布局 `default` & `compact` 间切换
173 | - `card_width` - 手动设置卡片的宽度 _(number)_
174 |
175 | > :warning: **重要:**
176 | > 如 [Percent Encoding](https://en.wikipedia.org/wiki/Percent-encoding) 所指定,语言名称应使用 uri 转义。
177 | > (例: `c++` 应该是 `c%2B%2B`, `jupyter notebook` 应该是 `jupyter%20notebook`, 等.)
178 |
179 | ---
180 |
181 | # GitHub 更多置顶
182 |
183 | GitHub 更多置顶 允许你在使用 GitHub readme profile 时,在个人资料中置顶多于 6 个 repo 。
184 |
185 | 是的!你不再受限于置顶最多 6 个存储库了。
186 |
187 | ### 使用细则
188 |
189 | 复制粘贴这段代码到你的 README 文件中,并更改链接。
190 |
191 | 端点: `api/pin?username=anuraghazra&repo=github-readme-stats`
192 |
193 | ```md
194 | [](https://github.com/anuraghazra/github-readme-stats)
195 | ```
196 |
197 | ### Demo
198 |
199 | [](https://github.com/anuraghazra/github-readme-stats)
200 |
201 | 使用 [show_owner](#自定义) 变量将 Repo 所有者的用户名包含在内。
202 |
203 | [](https://github.com/anuraghazra/github-readme-stats)
204 |
205 | # 热门语言卡片
206 |
207 | 热门语言卡片显示了 GitHub 用户常用的编程语言。
208 |
209 | _注意:热门语言并不表示我的技能水平或类似的水平,它是用来衡量用户在 github 上拥有最多代码的语言的一项指标,它是 github-readme-stats 的新特性_
210 |
211 | ### 使用细则
212 |
213 | 将此代码复制粘贴到您的 `README.md` 文件中,并修改链接。
214 |
215 | 端点: `api/top-langs?username=anuraghazra`
216 |
217 | ```md
218 | [](https://github.com/anuraghazra/github-readme-stats)
219 | ```
220 |
221 | ### 隐藏指定语言
222 |
223 | 可以使用 `?hide=language1,language2` 参数来隐藏指定的语言。
224 |
225 | ```md
226 | [](https://github.com/anuraghazra/github-readme-stats)
227 | ```
228 |
229 | ### 紧凑的语言卡片布局
230 |
231 | 你可以使用 `&layout=compact` 参数来改变卡片的样式。
232 |
233 | ```md
234 | [](https://github.com/anuraghazra/github-readme-stats)
235 | ```
236 |
237 | ### Demo
238 |
239 | [](https://github.com/anuraghazra/github-readme-stats)
240 |
241 | - 紧凑布局
242 |
243 | [](https://github.com/anuraghazra/github-readme-stats)
244 |
245 | ---
246 |
247 | ### 全部 Demos
248 |
249 | - 默认
250 |
251 | 
252 |
253 | - 隐藏指定统计
254 |
255 | 
256 |
257 | - 显示图标
258 |
259 | 
260 |
261 | - 包含全部提交
262 |
263 | 
264 |
265 | - 主题
266 |
267 | 从[默认主题](#主题)中进行选择
268 |
269 | 
270 |
271 | - 渐变
272 |
273 | 
274 |
275 | - 自定义统计卡片
276 |
277 | 
278 |
279 | - 自定义 repo 卡片
280 |
281 | 
282 |
283 | - 热门语言
284 |
285 | [](https://github.com/anuraghazra/github-readme-stats)
286 |
287 | ---
288 |
289 | ### 快速提示 (对齐 Repo 卡片)
290 |
291 | 你通常无法将图片靠边显示。为此,您可以使用以下方法:
292 |
293 | ```md
294 |
295 |
296 |
297 |
298 |
299 |
300 | ```
301 |
302 | ## 自己部署
303 |
304 | #### [Check Out Step By Step Video Tutorial By @codeSTACKr](https://youtu.be/n6d4KHSKqGk?t=107)
305 |
306 | 因为 GitHub 的 API 每个小时只允许 5 千次请求,我的 `https://github-readme-stats.vercel.app/api` 很有可能会触发限制。如果你将其托管在自己的 Vercel 服务器上,那么你就不必为此担心。点击 deploy 按钮来开始你的部署!
307 |
308 | 注意: 从 [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) 开始,我们应该能够处理超过 5 千次的请求,并且不会出现宕机问题 :D
309 |
310 | [](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats)
311 |
312 |
313 | 设置 Vercel 的指导
314 |
315 | 1. 前往 [vercel.com](https://vercel.com/)
316 | 1. 点击 `Log in`
317 | 
318 | 1. 点击 `Continue with GitHub` 通过 GitHub 进行登录
319 | 
320 | 1. 登录 GitHub 并允许访问所有存储库(如果系统这样提示)
321 | 1. Fork 这个仓库
322 | 1. 返回到你的 [Vercel dashboard](https://vercel.com/dashboard)
323 | 1. 选择 `Import Project`
324 | 
325 | 1. 选择 `Import Git Repository`
326 | 
327 | 1. 选择 root 并将所有内容保持不变,并且只需添加名为 PAT_1 的环境变量(如图所示),其中将包含一个个人访问令牌(PAT),你可以在[这里](https://github.com/settings/tokens/new)轻松创建(保留默认,并且只需要命名下,名字随便)
328 | 
329 | 1. 点击 deploy,这就完成了,查看你的域名就可使用 API 了!
330 |
331 |
332 |
333 | ## :sparkling_heart: 支持这个项目
334 |
335 | 我尽己所能地进行开源,并且我尽量回复每个在使用项目时需要帮助的人。很明显,这需要时间,但你可以免费享受这些。
336 |
337 | 然而, 如果你正在使用这个项目并感觉良好,或只是想要支持我继续开发,你可以通过如下方式:
338 |
339 | - 在你的 readme 中使用 github-readme-stats 时,链接指向这里 :D
340 | - Star 并 分享这个项目 :rocket:
341 | - [](https://www.paypal.me/anuraghazra) - 你可以通过 PayPal 一次性捐款. 我多半会买一杯 ~~咖啡~~ 茶. :tea:
342 |
343 | 谢谢! :heart:
344 |
345 | ---
346 |
347 | [](https://vercel.com?utm_source=github_readme_stats_team&utm_campaign=oss)
348 |
349 | 欢迎贡献! <3
350 |
351 | 用 :heart: 发电,用 JavaScript 制作。
352 |
--------------------------------------------------------------------------------
/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 | "preview-theme": "node scripts/preview-theme"
11 | },
12 | "author": "Anurag Hazra",
13 | "license": "MIT",
14 | "devDependencies": {
15 | "@actions/core": "^1.2.4",
16 | "@actions/github": "^4.0.0",
17 | "@testing-library/dom": "^7.20.0",
18 | "@testing-library/jest-dom": "^5.11.0",
19 | "axios": "^0.19.2",
20 | "axios-mock-adapter": "^1.18.1",
21 | "css-to-object": "^1.1.0",
22 | "husky": "^4.2.5",
23 | "jest": "^26.1.0",
24 | "parse-diff": "^0.7.0"
25 | },
26 | "dependencies": {
27 | "dotenv": "^8.2.0",
28 | "emoji-name-map": "^1.2.8",
29 | "github-username-regex": "^1.0.0",
30 | "prettier": "^2.1.2",
31 | "word-wrap": "^1.2.3"
32 | },
33 | "husky": {
34 | "hooks": {
35 | "pre-commit": "npm test"
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/powered-by-vercel.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/scripts/generate-theme-doc.js:
--------------------------------------------------------------------------------
1 | const theme = require("../themes/index");
2 | const fs = require("fs");
3 |
4 | const TARGET_FILE = "./themes/README.md";
5 | const REPO_CARD_LINKS_FLAG = "";
6 | const STAT_CARD_LINKS_FLAG = "";
7 |
8 | const STAT_CARD_TABLE_FLAG = "";
9 | const REPO_CARD_TABLE_FLAG = "";
10 |
11 | const THEME_TEMPLATE = `## Available Themes
12 |
13 |
14 |
15 | With inbuilt themes you can customize the look of the card without doing any manual customization.
16 |
17 | Use \`?theme=THEME_NAME\` parameter like so :-
18 |
19 | \`\`\`md
20 | 
21 | \`\`\`
22 |
23 | ## Stats
24 |
25 | > These themes work both for the Stats Card and Repo Card.
26 |
27 | | | | |
28 | | :--: | :--: | :--: |
29 | ${STAT_CARD_TABLE_FLAG}
30 |
31 | ## Repo Card
32 |
33 | > These themes work both for the Stats Card and Repo Card.
34 |
35 | | | | |
36 | | :--: | :--: | :--: |
37 | ${REPO_CARD_TABLE_FLAG}
38 |
39 | ${STAT_CARD_LINKS_FLAG}
40 |
41 | ${REPO_CARD_LINKS_FLAG}
42 |
43 |
44 | [add-theme]: https://github.com/anuraghazra/github-readme-stats/edit/master/themes/index.js
45 |
46 | Wanted to add a new theme? Consider reading the [contribution guidelines](../CONTRIBUTING.md#themes-contribution) :D
47 | `;
48 |
49 | const createRepoMdLink = (theme) => {
50 | return `\n[${theme}_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=${theme}`;
51 | };
52 | const createStatMdLink = (theme) => {
53 | return `\n[${theme}]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=${theme}`;
54 | };
55 |
56 | const generateLinks = (fn) => {
57 | return Object.keys(theme)
58 | .map((name) => fn(name))
59 | .join("");
60 | };
61 |
62 | const createTableItem = ({ link, label, isRepoCard }) => {
63 | if (!link || !label) return "";
64 | return `\`${label}\` ![${link}][${link}${isRepoCard ? "_repo" : ""}]`;
65 | };
66 | const generateTable = ({ isRepoCard }) => {
67 | const rows = [];
68 | const themes = Object.keys(theme).filter(
69 | (name) => name !== (!isRepoCard ? "default_repocard" : "default"),
70 | );
71 |
72 | for (let i = 0; i < themes.length; i += 3) {
73 | const one = themes[i];
74 | const two = themes[i + 1];
75 | const three = themes[i + 2];
76 |
77 | let tableItem1 = createTableItem({ link: one, label: one, isRepoCard });
78 | let tableItem2 = createTableItem({ link: two, label: two, isRepoCard });
79 | let tableItem3 = createTableItem({ link: three, label: three, isRepoCard });
80 |
81 | if (three === undefined) {
82 | tableItem3 = `[Add your theme][add-theme]`;
83 | }
84 | rows.push(`| ${tableItem1} | ${tableItem2} | ${tableItem3} |`);
85 |
86 | // if it's the last row & the row has no empty space push a new row
87 | if (three && i + 3 === themes.length) {
88 | rows.push(`| [Add your theme][add-theme] | | |`);
89 | }
90 | }
91 |
92 | return rows.join("\n");
93 | };
94 |
95 | const buildReadme = () => {
96 | return THEME_TEMPLATE.split("\n")
97 | .map((line) => {
98 | if (line.includes(REPO_CARD_LINKS_FLAG)) {
99 | return generateLinks(createRepoMdLink);
100 | }
101 | if (line.includes(STAT_CARD_LINKS_FLAG)) {
102 | return generateLinks(createStatMdLink);
103 | }
104 | if (line.includes(REPO_CARD_TABLE_FLAG)) {
105 | return generateTable({ isRepoCard: true });
106 | }
107 | if (line.includes(STAT_CARD_TABLE_FLAG)) {
108 | return generateTable({ isRepoCard: false });
109 | }
110 | return line;
111 | })
112 | .join("\n");
113 | };
114 |
115 | fs.writeFileSync(TARGET_FILE, buildReadme());
116 |
--------------------------------------------------------------------------------
/scripts/preview-theme.js:
--------------------------------------------------------------------------------
1 | const core = require("@actions/core");
2 | const github = require("@actions/github");
3 | const parse = require("parse-diff");
4 | require("dotenv").config();
5 |
6 | function getPrNumber() {
7 | const pullRequest = github.context.payload.pull_request;
8 | if (!pullRequest) {
9 | return undefined;
10 | }
11 |
12 | return pullRequest.number;
13 | }
14 |
15 | const themeContribGuidelines = `
16 | \r> Hi thanks for the theme contribution, please read our theme contribution guidelines
17 |
18 | \r> We are currently only accepting color combinations from any vscode theme or which has good color combination to minimize bloating the themes collection.
19 |
20 | \r> Also note that if this theme is exclusively for your personal use then instead of adding it to our theme collection you can use card [customization options](https://github.com/anuraghazra/github-readme-stats#customization)
21 | \r> Read our [contribution guidelines](https://github.com/anuraghazra/github-readme-stats/blob/master/CONTRIBUTING.md) for more info
22 | `;
23 |
24 | async function run() {
25 | try {
26 | const token = core.getInput("token");
27 | const octokit = github.getOctokit(token || process.env.PERSONAL_TOKEN);
28 | const pullRequestId = getPrNumber();
29 |
30 | if (!pullRequestId) {
31 | console.log("PR not found");
32 | return;
33 | }
34 |
35 | let res = await octokit.pulls.get({
36 | owner: "anuraghazra",
37 | repo: "github-readme-stats",
38 | pull_number: pullRequestId,
39 | mediaType: {
40 | format: "diff",
41 | },
42 | });
43 |
44 | let diff = parse(res.data);
45 | let colorStrings = diff
46 | .find((file) => file.to === "themes/index.js")
47 | .chunks[0].changes.filter((c) => c.type === "add")
48 | .map((c) => c.content.replace("+", ""))
49 | .join("");
50 |
51 | let matches = colorStrings.match(/(title_color:.*bg_color.*\")/);
52 | let colors = matches && matches[0].split(",");
53 |
54 | if (!colors) {
55 | await octokit.issues.createComment({
56 | owner: "anuraghazra",
57 | repo: "github-readme-stats",
58 | body: `
59 | \rTheme preview (bot)
60 |
61 | \rCannot create theme preview
62 |
63 | ${themeContribGuidelines}
64 | `,
65 | issue_number: pullRequestId,
66 | });
67 | return;
68 | }
69 | colors = colors.map((color) =>
70 | color.replace(/.*\:\s/, "").replace(/\"/g, ""),
71 | );
72 |
73 | const titleColor = colors[0];
74 | const iconColor = colors[1];
75 | const textColor = colors[2];
76 | const bgColor = colors[3];
77 | const url = `https://github-readme-stats.vercel.app/api?username=anuraghazra&title_color=${titleColor}&icon_color=${iconColor}&text_color=${textColor}&bg_color=${bgColor}&show_icons=true`;
78 |
79 | await octokit.issues.createComment({
80 | owner: "anuraghazra",
81 | repo: "github-readme-stats",
82 | body: `
83 | \rTheme preview (bot)
84 |
85 | \ntitle_color: #${titleColor}
| icon_color: #${iconColor}
| text_color: #${textColor}
| bg_color: #${bgColor}
86 |
87 | \rLink: ${url}
88 |
89 | \r[](${url})
90 |
91 | ${themeContribGuidelines}
92 | `,
93 | issue_number: pullRequestId,
94 | });
95 | } catch (error) {
96 | console.log(error);
97 | }
98 | }
99 |
100 | run();
101 |
--------------------------------------------------------------------------------
/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/cards/repo-card.js:
--------------------------------------------------------------------------------
1 | const toEmoji = require("emoji-name-map");
2 | const {
3 | kFormatter,
4 | encodeHTML,
5 | getCardColors,
6 | FlexLayout,
7 | wrapTextMultiline,
8 | } = require("../common/utils");
9 | const I18n = require("../common/I18n");
10 | const Card = require("../common/Card");
11 | const icons = require("../common/icons");
12 | const { repoCardLocales } = require("../translations");
13 |
14 | const renderRepoCard = (repo, options = {}) => {
15 | const {
16 | name,
17 | nameWithOwner,
18 | description,
19 | primaryLanguage,
20 | stargazers,
21 | isArchived,
22 | isTemplate,
23 | forkCount,
24 | } = repo;
25 | const {
26 | hide_border = false,
27 | title_color,
28 | icon_color,
29 | text_color,
30 | bg_color,
31 | show_owner,
32 | theme = "default_repocard",
33 | border_radius,
34 | locale,
35 | } = options;
36 |
37 | const header = show_owner ? nameWithOwner : name;
38 | const langName = (primaryLanguage && primaryLanguage.name) || "Unspecified";
39 | const langColor = (primaryLanguage && primaryLanguage.color) || "#333";
40 |
41 | const shiftText = langName.length > 15 ? 0 : 30;
42 |
43 | let desc = description || "No description provided";
44 |
45 | // parse emojis to unicode
46 | desc = desc.replace(/:\w+:/gm, (emoji) => {
47 | return toEmoji.get(emoji) || "";
48 | });
49 |
50 | const multiLineDescription = wrapTextMultiline(desc);
51 | const descriptionLines = multiLineDescription.length;
52 | const lineHeight = 10;
53 |
54 | const height =
55 | (descriptionLines > 1 ? 120 : 110) + descriptionLines * lineHeight;
56 |
57 | const i18n = new I18n({
58 | locale,
59 | translations: repoCardLocales,
60 | });
61 |
62 | // returns theme based colors with proper overrides and defaults
63 | const { titleColor, textColor, iconColor, bgColor } = getCardColors({
64 | title_color,
65 | icon_color,
66 | text_color,
67 | bg_color,
68 | theme,
69 | });
70 |
71 | const totalStars = kFormatter(stargazers.totalCount);
72 | const totalForks = kFormatter(forkCount);
73 |
74 | const getBadgeSVG = (label) => `
75 |
76 |
77 |
84 | ${label}
85 |
86 |
87 | `;
88 |
89 | const svgLanguage = primaryLanguage
90 | ? `
91 |
92 |
93 | ${langName}
94 |
95 | `
96 | : "";
97 |
98 | const iconWithLabel = (icon, label, testid) => {
99 | return `
100 |
103 | ${label}
104 | `;
105 | };
106 | const svgStars =
107 | stargazers.totalCount > 0 &&
108 | iconWithLabel(icons.star, totalStars, "stargazers");
109 | const svgForks =
110 | forkCount > 0 && iconWithLabel(icons.fork, totalForks, "forkcount");
111 |
112 | const starAndForkCount = FlexLayout({
113 | items: [svgStars, svgForks],
114 | gap: 65,
115 | }).join("");
116 |
117 | const card = new Card({
118 | defaultTitle: header,
119 | titlePrefixIcon: icons.contribs,
120 | width: 400,
121 | height,
122 | border_radius,
123 | colors: {
124 | titleColor,
125 | textColor,
126 | iconColor,
127 | bgColor,
128 | },
129 | });
130 |
131 | card.disableAnimations();
132 | card.setHideBorder(hide_border);
133 | card.setHideTitle(false);
134 | card.setCSS(`
135 | .description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
136 | .gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
137 | .icon { fill: ${iconColor} }
138 | .badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
139 | .badge rect { opacity: 0.2 }
140 | `);
141 |
142 | return card.render(`
143 | ${
144 | isTemplate
145 | ? getBadgeSVG(i18n.t("repocard.template"))
146 | : isArchived
147 | ? getBadgeSVG(i18n.t("repocard.archived"))
148 | : ""
149 | }
150 |
151 |
152 | ${multiLineDescription
153 | .map((line) => `${encodeHTML(line)}`)
154 | .join("")}
155 |
156 |
157 |
158 | ${svgLanguage}
159 |
160 |
164 | ${starAndForkCount}
165 |
166 |
167 | `);
168 | };
169 |
170 | module.exports = renderRepoCard;
171 |
--------------------------------------------------------------------------------
/src/cards/stats-card.js:
--------------------------------------------------------------------------------
1 | const I18n = require("../common/I18n");
2 | const Card = require("../common/Card");
3 | const icons = require("../common/icons");
4 | const { getStyles } = require("../getStyles");
5 | const { statCardLocales } = require("../translations");
6 | const {
7 | kFormatter,
8 | FlexLayout,
9 | clampValue,
10 | measureText,
11 | getCardColors,
12 | } = require("../common/utils");
13 |
14 | const createTextNode = ({
15 | icon,
16 | label,
17 | value,
18 | id,
19 | index,
20 | showIcons,
21 | shiftValuePos,
22 | }) => {
23 | const kValue = kFormatter(value);
24 | const staggerDelay = (index + 3) * 150;
25 |
26 | const labelOffset = showIcons ? `x="25"` : "";
27 | const iconSvg = showIcons
28 | ? `
29 |
32 | `
33 | : "";
34 | return `
35 |
36 | ${iconSvg}
37 | ${label}:
38 | ${kValue}
44 |
45 | `;
46 | };
47 |
48 | const renderStatsCard = (stats = {}, options = { hide: [] }) => {
49 | const {
50 | name,
51 | totalStars,
52 | totalCommits,
53 | totalIssues,
54 | totalPRs,
55 | contributedTo,
56 | rank,
57 | } = stats;
58 | const {
59 | hide = [],
60 | show_icons = false,
61 | hide_title = false,
62 | hide_border = false,
63 | hide_rank = false,
64 | include_all_commits = false,
65 | line_height = 25,
66 | title_color,
67 | icon_color,
68 | text_color,
69 | bg_color,
70 | theme = "default",
71 | custom_title,
72 | border_radius,
73 | locale,
74 | disable_animations = false,
75 | } = options;
76 |
77 | const lheight = parseInt(line_height, 10);
78 |
79 | // returns theme based colors with proper overrides and defaults
80 | const { titleColor, textColor, iconColor, bgColor } = getCardColors({
81 | title_color,
82 | icon_color,
83 | text_color,
84 | bg_color,
85 | theme,
86 | });
87 |
88 | const apostrophe = ["x", "s"].includes(name.slice(-1).toLocaleLowerCase())
89 | ? ""
90 | : "s";
91 | const i18n = new I18n({
92 | locale,
93 | translations: statCardLocales({ name, apostrophe }),
94 | });
95 |
96 | // Meta data for creating text nodes with createTextNode function
97 | const STATS = {
98 | stars: {
99 | icon: icons.star,
100 | label: i18n.t("statcard.totalstars"),
101 | value: totalStars,
102 | id: "stars",
103 | },
104 | commits: {
105 | icon: icons.commits,
106 | label: `${i18n.t("statcard.commits")}${
107 | include_all_commits ? "" : ` (${new Date().getFullYear()})`
108 | }`,
109 | value: totalCommits,
110 | id: "commits",
111 | },
112 | prs: {
113 | icon: icons.prs,
114 | label: i18n.t("statcard.prs"),
115 | value: totalPRs,
116 | id: "prs",
117 | },
118 | issues: {
119 | icon: icons.issues,
120 | label: i18n.t("statcard.issues"),
121 | value: totalIssues,
122 | id: "issues",
123 | },
124 | contribs: {
125 | icon: icons.contribs,
126 | label: i18n.t("statcard.contribs"),
127 | value: contributedTo,
128 | id: "contribs",
129 | },
130 | };
131 |
132 | const longLocales = ["cn", "es", "fr", "pt-br", "ru", "uk-ua", "id", "my", "pl"];
133 | const isLongLocale = longLocales.includes(locale) === true;
134 |
135 | // filter out hidden stats defined by user & create the text nodes
136 | const statItems = Object.keys(STATS)
137 | .filter((key) => !hide.includes(key))
138 | .map((key, index) =>
139 | // create the text nodes, and pass index so that we can calculate the line spacing
140 | createTextNode({
141 | ...STATS[key],
142 | index,
143 | showIcons: show_icons,
144 | shiftValuePos:
145 | (!include_all_commits ? 50 : 20) + (isLongLocale ? 50 : 0),
146 | }),
147 | );
148 |
149 | // Calculate the card height depending on how many items there are
150 | // but if rank circle is visible clamp the minimum height to `150`
151 | let height = Math.max(
152 | 45 + (statItems.length + 1) * lheight,
153 | hide_rank ? 0 : 150,
154 | );
155 |
156 | // Conditionally rendered elements
157 | const rankCircle = hide_rank
158 | ? ""
159 | : `
161 |
162 |
163 |
164 |
171 | ${rank.level}
172 |
173 |
174 | `;
175 |
176 | // the better user's score the the rank will be closer to zero so
177 | // subtracting 100 to get the progress in 100%
178 | const progress = 100 - rank.score;
179 | const cssStyles = getStyles({
180 | titleColor,
181 | textColor,
182 | iconColor,
183 | show_icons,
184 | progress,
185 | });
186 |
187 | const calculateTextWidth = () => {
188 | return measureText(custom_title ? custom_title : i18n.t("statcard.title"));
189 | };
190 |
191 | const width = hide_rank
192 | ? clampValue(
193 | 50 /* padding */ + calculateTextWidth() * 2,
194 | 270 /* min */,
195 | Infinity,
196 | )
197 | : 495;
198 |
199 | const card = new Card({
200 | customTitle: custom_title,
201 | defaultTitle: i18n.t("statcard.title"),
202 | width,
203 | height,
204 | border_radius,
205 | colors: {
206 | titleColor,
207 | textColor,
208 | iconColor,
209 | bgColor,
210 | },
211 | });
212 |
213 | card.setHideBorder(hide_border);
214 | card.setHideTitle(hide_title);
215 | card.setCSS(cssStyles);
216 |
217 | if (disable_animations) card.disableAnimations();
218 |
219 | return card.render(`
220 | ${rankCircle}
221 |
222 |
229 | `);
230 | };
231 |
232 | module.exports = renderStatsCard;
233 |
--------------------------------------------------------------------------------
/src/cards/top-languages-card.js:
--------------------------------------------------------------------------------
1 | const Card = require("../common/Card");
2 | const { getCardColors, FlexLayout } = require("../common/utils");
3 | const { createProgressNode } = require("../common/createProgressNode");
4 | const { langCardLocales } = require("../translations");
5 | const I18n = require("../common/I18n");
6 |
7 | const createProgressTextNode = ({ width, color, name, progress }) => {
8 | const paddingRight = 95;
9 | const progressTextX = width - paddingRight + 10;
10 | const progressWidth = width - paddingRight;
11 |
12 | return `
13 | ${name}
14 | ${progress}%
15 | ${createProgressNode({
16 | x: 0,
17 | y: 25,
18 | color,
19 | width: progressWidth,
20 | progress,
21 | progressBarBackgroundColor: "#ddd",
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 | hide_border,
67 | card_width,
68 | title_color,
69 | text_color,
70 | bg_color,
71 | hide,
72 | theme,
73 | layout,
74 | custom_title,
75 | locale,
76 | border_radius
77 | } = options;
78 |
79 | const i18n = new I18n({
80 | locale,
81 | translations: langCardLocales,
82 | });
83 |
84 | let langs = Object.values(topLangs);
85 | let langsToHide = {};
86 |
87 | // populate langsToHide map for quick lookup
88 | // while filtering out
89 | if (hide) {
90 | hide.forEach((langName) => {
91 | langsToHide[lowercaseTrim(langName)] = true;
92 | });
93 | }
94 |
95 | // filter out langauges to be hidden
96 | langs = langs
97 | .sort((a, b) => b.size - a.size)
98 | .filter((lang) => {
99 | return !langsToHide[lowercaseTrim(lang.name)];
100 | });
101 |
102 | const totalLanguageSize = langs.reduce((acc, curr) => {
103 | return acc + curr.size;
104 | }, 0);
105 |
106 | // returns theme based colors with proper overrides and defaults
107 | const { titleColor, textColor, bgColor } = getCardColors({
108 | title_color,
109 | text_color,
110 | bg_color,
111 | theme,
112 | });
113 |
114 | let width = isNaN(card_width) ? 300 : card_width;
115 | let height = 45 + (langs.length + 1) * 40;
116 |
117 | let finalLayout = "";
118 |
119 | // RENDER COMPACT LAYOUT
120 | if (layout === "compact") {
121 | width = width + 50;
122 | height = 90 + Math.round(langs.length / 2) * 25;
123 |
124 | // progressOffset holds the previous language's width and used to offset the next language
125 | // so that we can stack them one after another, like this: [--][----][---]
126 | let progressOffset = 0;
127 | const compactProgressBar = langs
128 | .map((lang) => {
129 | const percentage = (
130 | (lang.size / totalLanguageSize) *
131 | (width - 50)
132 | ).toFixed(2);
133 |
134 | const progress =
135 | percentage < 10 ? parseFloat(percentage) + 10 : percentage;
136 |
137 | const output = `
138 |
147 | `;
148 | progressOffset += parseFloat(percentage);
149 | return output;
150 | })
151 | .join("");
152 |
153 | finalLayout = `
154 |
155 |
158 |
159 | ${compactProgressBar}
160 | ${createLanguageTextNode({
161 | x: 0,
162 | y: 25,
163 | langs,
164 | totalSize: totalLanguageSize,
165 | }).join("")}
166 | `;
167 | } else {
168 | finalLayout = FlexLayout({
169 | items: langs.map((lang) => {
170 | return createProgressTextNode({
171 | width: width,
172 | name: lang.name,
173 | color: lang.color || "#858585",
174 | progress: ((lang.size / totalLanguageSize) * 100).toFixed(2),
175 | });
176 | }),
177 | gap: 40,
178 | direction: "column",
179 | }).join("");
180 | }
181 |
182 | const card = new Card({
183 | customTitle: custom_title,
184 | defaultTitle: i18n.t("langcard.title"),
185 | width,
186 | height,
187 | border_radius,
188 | colors: {
189 | titleColor,
190 | textColor,
191 | bgColor,
192 | },
193 | });
194 |
195 | card.disableAnimations();
196 | card.setHideBorder(hide_border);
197 | card.setHideTitle(hide_title);
198 | card.setCSS(`
199 | .lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
200 | `);
201 |
202 | return card.render(`
203 |
206 | `);
207 | };
208 |
209 | module.exports = renderTopLanguages;
210 |
--------------------------------------------------------------------------------
/src/cards/wakatime-card.js:
--------------------------------------------------------------------------------
1 | const Card = require("../common/Card");
2 | const I18n = require("../common/I18n");
3 | const { getStyles } = require("../getStyles");
4 | const { wakatimeCardLocales } = require("../translations");
5 | const { getCardColors, FlexLayout } = require("../common/utils");
6 | const { createProgressNode } = require("../common/createProgressNode");
7 | const languageColors = require("../common/languageColors.json");
8 |
9 | const noCodingActivityNode = ({ color, text }) => {
10 | return `
11 | ${text}
12 | `;
13 | };
14 |
15 | const createCompactLangNode = ({ lang, totalSize, x, y }) => {
16 | const color = languageColors[lang.name] || "#858585";
17 |
18 | return `
19 |
20 |
21 |
22 | ${lang.name} - ${lang.text}
23 |
24 |
25 | `;
26 | };
27 |
28 | const createLanguageTextNode = ({ langs, totalSize, x, y }) => {
29 | return langs.map((lang, index) => {
30 | if (index % 2 === 0) {
31 | return createCompactLangNode({
32 | lang,
33 | x: 25,
34 | y: 12.5 * index + y,
35 | totalSize,
36 | index,
37 | });
38 | }
39 | return createCompactLangNode({
40 | lang,
41 | x: 230,
42 | y: 12.5 + 12.5 * index,
43 | totalSize,
44 | index,
45 | });
46 | });
47 | };
48 |
49 | const createTextNode = ({
50 | id,
51 | label,
52 | value,
53 | index,
54 | percent,
55 | hideProgress,
56 | progressBarColor,
57 | progressBarBackgroundColor,
58 | }) => {
59 | const staggerDelay = (index + 3) * 150;
60 |
61 | const cardProgress = hideProgress
62 | ? null
63 | : createProgressNode({
64 | x: 110,
65 | y: 4,
66 | progress: percent,
67 | color: progressBarColor,
68 | width: 220,
69 | name: label,
70 | progressBarBackgroundColor,
71 | });
72 |
73 | return `
74 |
75 | ${label}:
76 | ${value}
82 | ${cardProgress}
83 |
84 | `;
85 | };
86 |
87 | const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
88 | const { languages } = stats;
89 | const {
90 | hide_title = false,
91 | hide_border = false,
92 | line_height = 25,
93 | title_color,
94 | icon_color,
95 | text_color,
96 | bg_color,
97 | theme = "default",
98 | hide_progress,
99 | custom_title,
100 | locale,
101 | layout,
102 | border_radius
103 | } = options;
104 |
105 | const i18n = new I18n({
106 | locale,
107 | translations: wakatimeCardLocales,
108 | });
109 |
110 | const lheight = parseInt(line_height, 10);
111 |
112 | // returns theme based colors with proper overrides and defaults
113 | const { titleColor, textColor, iconColor, bgColor } = getCardColors({
114 | title_color,
115 | icon_color,
116 | text_color,
117 | bg_color,
118 | theme,
119 | });
120 |
121 | const statItems = languages
122 | ? languages
123 | .filter((language) => language.hours || language.minutes)
124 | .map((language) => {
125 | return createTextNode({
126 | id: language.name,
127 | label: language.name,
128 | value: language.text,
129 | percent: language.percent,
130 | progressBarColor: titleColor,
131 | progressBarBackgroundColor: textColor,
132 | hideProgress: hide_progress,
133 | });
134 | })
135 | : [];
136 |
137 | // Calculate the card height depending on how many items there are
138 | // but if rank circle is visible clamp the minimum height to `150`
139 | let height = Math.max(45 + (statItems.length + 1) * lheight, 150);
140 |
141 | const cssStyles = getStyles({
142 | titleColor,
143 | textColor,
144 | iconColor,
145 | });
146 |
147 | let finalLayout = "";
148 |
149 | let width = 440;
150 |
151 | // RENDER COMPACT LAYOUT
152 | if (layout === "compact") {
153 | width = width + 50;
154 | height = 90 + Math.round(languages.length / 2) * 25;
155 |
156 | // progressOffset holds the previous language's width and used to offset the next language
157 | // so that we can stack them one after another, like this: [--][----][---]
158 | let progressOffset = 0;
159 | const compactProgressBar = languages
160 | .map((lang) => {
161 | // const progress = (width * lang.percent) / 100;
162 | const progress = ((width - 25) * lang.percent) / 100;
163 |
164 | const languageColor = languageColors[lang.name] || "#858585";
165 |
166 | const output = `
167 |
176 | `;
177 | progressOffset += progress;
178 | return output;
179 | })
180 | .join("");
181 |
182 | finalLayout = `
183 |
184 |
185 |
186 | ${compactProgressBar}
187 | ${createLanguageTextNode({
188 | x: 0,
189 | y: 25,
190 | langs: languages,
191 | totalSize: 100,
192 | }).join("")}
193 | `;
194 | } else {
195 | finalLayout = FlexLayout({
196 | items: statItems.length
197 | ? statItems
198 | : [
199 | noCodingActivityNode({
200 | color: textColor,
201 | text: i18n.t("wakatimecard.nocodingactivity"),
202 | }),
203 | ],
204 | gap: lheight,
205 | direction: "column",
206 | }).join("");
207 | }
208 |
209 | const card = new Card({
210 | customTitle: custom_title,
211 | defaultTitle: i18n.t("wakatimecard.title"),
212 | width: 495,
213 | height,
214 | border_radius,
215 | colors: {
216 | titleColor,
217 | textColor,
218 | iconColor,
219 | bgColor,
220 | },
221 | });
222 |
223 | card.setHideBorder(hide_border);
224 | card.setHideTitle(hide_title);
225 | card.setCSS(
226 | `
227 | ${cssStyles}
228 | .lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
229 | `,
230 | );
231 |
232 | return card.render(`
233 |
236 | `);
237 | };
238 |
239 | module.exports = renderWakatimeCard;
240 | exports.createProgressNode = createProgressNode;
241 |
--------------------------------------------------------------------------------
/src/common/Card.js:
--------------------------------------------------------------------------------
1 | const { FlexLayout, encodeHTML } = require("../common/utils");
2 | const { getAnimations } = require("../getStyles");
3 |
4 | class Card {
5 | constructor({
6 | width = 100,
7 | height = 100,
8 | border_radius = 4.5,
9 | colors = {},
10 | customTitle,
11 | defaultTitle = "",
12 | titlePrefixIcon,
13 | }) {
14 | this.width = width;
15 | this.height = height;
16 |
17 | this.hideBorder = false;
18 | this.hideTitle = false;
19 |
20 | this.border_radius = border_radius;
21 |
22 | // returns theme based colors with proper overrides and defaults
23 | this.colors = colors;
24 | this.title =
25 | customTitle !== undefined
26 | ? encodeHTML(customTitle)
27 | : encodeHTML(defaultTitle);
28 |
29 | this.css = "";
30 |
31 | this.paddingX = 25;
32 | this.paddingY = 35;
33 | this.titlePrefixIcon = titlePrefixIcon;
34 | this.animations = true;
35 | }
36 |
37 | disableAnimations() {
38 | this.animations = false;
39 | }
40 |
41 | setCSS(value) {
42 | this.css = value;
43 | }
44 |
45 | setHideBorder(value) {
46 | this.hideBorder = value;
47 | }
48 |
49 | setHideTitle(value) {
50 | this.hideTitle = value;
51 | if (value) {
52 | this.height -= 30;
53 | }
54 | }
55 |
56 | setTitle(text) {
57 | this.title = text;
58 | }
59 |
60 | renderTitle() {
61 | const titleText = `
62 |
68 | `;
69 |
70 | const prefixIcon = `
71 |
82 | `;
83 | return `
84 |
88 | ${FlexLayout({
89 | items: [this.titlePrefixIcon && prefixIcon, titleText],
90 | gap: 25,
91 | }).join("")}
92 |
93 | `;
94 | }
95 |
96 | renderGradient() {
97 | if (typeof this.colors.bgColor !== "object") return;
98 |
99 | const gradients = this.colors.bgColor.slice(1);
100 | return typeof this.colors.bgColor === "object"
101 | ? `
102 |
103 |
107 | ${gradients.map((grad, index) => {
108 | let offset = (index * 100) / (gradients.length - 1);
109 | return ``;
110 | })}
111 |
112 |
113 | `
114 | : "";
115 | }
116 |
117 | render(body) {
118 | return `
119 |
171 | `;
172 | }
173 | }
174 |
175 | module.exports = Card;
176 |
--------------------------------------------------------------------------------
/src/common/I18n.js:
--------------------------------------------------------------------------------
1 | class I18n {
2 | constructor({ locale, translations }) {
3 | this.locale = locale;
4 | this.translations = translations;
5 | this.fallbackLocale = "en";
6 | }
7 |
8 | t(str) {
9 | if (!this.translations[str]) {
10 | throw new Error(`${str} Translation string not found`);
11 | }
12 |
13 | if (!this.translations[str][this.locale || this.fallbackLocale]) {
14 | throw new Error(`${str} Translation locale not found`);
15 | }
16 |
17 | return this.translations[str][this.locale || this.fallbackLocale];
18 | }
19 | }
20 |
21 | module.exports = I18n;
22 |
--------------------------------------------------------------------------------
/src/common/blacklist.js:
--------------------------------------------------------------------------------
1 | const blacklist = ["renovate-bot", "technote-space", "sw-yx"];
2 |
3 | module.exports = blacklist;
4 |
--------------------------------------------------------------------------------
/src/common/createProgressNode.js:
--------------------------------------------------------------------------------
1 | const { clampValue } = require("../common/utils");
2 |
3 | const createProgressNode = ({
4 | x,
5 | y,
6 | width,
7 | color,
8 | progress,
9 | progressBarBackgroundColor,
10 | }) => {
11 | const progressPercentage = clampValue(progress, 2, 100);
12 |
13 | return `
14 |
25 | `;
26 | };
27 |
28 | exports.createProgressNode = createProgressNode;
29 |
--------------------------------------------------------------------------------
/src/common/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/common/languageColors.json:
--------------------------------------------------------------------------------
1 | {
2 | "1C Enterprise": "#814CCC",
3 | "4D": null,
4 | "ABAP": "#E8274B",
5 | "ActionScript": "#882B0F",
6 | "Ada": "#02f88c",
7 | "Agda": "#315665",
8 | "AGS Script": "#B9D9FF",
9 | "AL": "#3AA2B5",
10 | "Alloy": "#64C800",
11 | "Alpine Abuild": null,
12 | "AMPL": "#E6EFBB",
13 | "AngelScript": "#C7D7DC",
14 | "ANTLR": "#9DC3FF",
15 | "Apex": "#1797c0",
16 | "API Blueprint": "#2ACCA8",
17 | "APL": "#5A8164",
18 | "Apollo Guidance Computer": "#0B3D91",
19 | "AppleScript": "#101F1F",
20 | "Arc": "#aa2afe",
21 | "ASL": null,
22 | "ASP.NET": "#9400ff",
23 | "AspectJ": "#a957b0",
24 | "Assembly": "#6E4C13",
25 | "Asymptote": "#ff0000",
26 | "ATS": "#1ac620",
27 | "Augeas": null,
28 | "AutoHotkey": "#6594b9",
29 | "AutoIt": "#1C3552",
30 | "Awk": null,
31 | "Ballerina": "#FF5000",
32 | "Batchfile": "#C1F12E",
33 | "Befunge": null,
34 | "Bison": "#6A463F",
35 | "BitBake": null,
36 | "Blade": "#f7523f",
37 | "BlitzBasic": null,
38 | "BlitzMax": "#cd6400",
39 | "Bluespec": null,
40 | "Boo": "#d4bec1",
41 | "Brainfuck": "#2F2530",
42 | "Brightscript": null,
43 | "C": "#555555",
44 | "C#": "#178600",
45 | "C++": "#f34b7d",
46 | "C2hs Haskell": null,
47 | "Cap'n Proto": null,
48 | "CartoCSS": null,
49 | "Ceylon": "#dfa535",
50 | "Chapel": "#8dc63f",
51 | "Charity": null,
52 | "ChucK": null,
53 | "Cirru": "#ccccff",
54 | "Clarion": "#db901e",
55 | "Classic ASP": "#6a40fd",
56 | "Clean": "#3F85AF",
57 | "Click": "#E4E6F3",
58 | "CLIPS": null,
59 | "Clojure": "#db5855",
60 | "CMake": null,
61 | "COBOL": null,
62 | "CodeQL": null,
63 | "CoffeeScript": "#244776",
64 | "ColdFusion": "#ed2cd6",
65 | "ColdFusion CFC": "#ed2cd6",
66 | "Common Lisp": "#3fb68b",
67 | "Common Workflow Language": "#B5314C",
68 | "Component Pascal": "#B0CE4E",
69 | "Cool": null,
70 | "Coq": null,
71 | "Crystal": "#000100",
72 | "CSON": "#244776",
73 | "Csound": null,
74 | "Csound Document": null,
75 | "Csound Score": null,
76 | "CSS": "#563d7c",
77 | "Cuda": "#3A4E3A",
78 | "CWeb": null,
79 | "Cycript": null,
80 | "Cython": null,
81 | "D": "#ba595e",
82 | "Dafny": "#FFEC25",
83 | "Dart": "#00B4AB",
84 | "DataWeave": "#003a52",
85 | "Dhall": "#dfafff",
86 | "DIGITAL Command Language": null,
87 | "DM": "#447265",
88 | "Dockerfile": "#384d54",
89 | "Dogescript": "#cca760",
90 | "DTrace": null,
91 | "Dylan": "#6c616e",
92 | "E": "#ccce35",
93 | "eC": "#913960",
94 | "ECL": "#8a1267",
95 | "ECLiPSe": null,
96 | "Eiffel": "#4d6977",
97 | "EJS": "#a91e50",
98 | "Elixir": "#6e4a7e",
99 | "Elm": "#60B5CC",
100 | "Emacs Lisp": "#c065db",
101 | "EmberScript": "#FFF4F3",
102 | "EQ": "#a78649",
103 | "Erlang": "#B83998",
104 | "F#": "#b845fc",
105 | "F*": "#572e30",
106 | "Factor": "#636746",
107 | "Fancy": "#7b9db4",
108 | "Fantom": "#14253c",
109 | "Faust": "#c37240",
110 | "Filebench WML": null,
111 | "Filterscript": null,
112 | "fish": null,
113 | "FLUX": "#88ccff",
114 | "Forth": "#341708",
115 | "Fortran": "#4d41b1",
116 | "Fortran Free Form": null,
117 | "FreeMarker": "#0050b2",
118 | "Frege": "#00cafe",
119 | "Futhark": "#5f021f",
120 | "G-code": "#D08CF2",
121 | "Game Maker Language": "#71b417",
122 | "GAML": "#FFC766",
123 | "GAMS": null,
124 | "GAP": null,
125 | "GCC Machine Description": null,
126 | "GDB": null,
127 | "GDScript": "#355570",
128 | "Genie": "#fb855d",
129 | "Genshi": null,
130 | "Gentoo Ebuild": null,
131 | "Gentoo Eclass": null,
132 | "Gherkin": "#5B2063",
133 | "GLSL": null,
134 | "Glyph": "#c1ac7f",
135 | "Gnuplot": "#f0a9f0",
136 | "Go": "#00ADD8",
137 | "Golo": "#88562A",
138 | "Gosu": "#82937f",
139 | "Grace": null,
140 | "Grammatical Framework": "#ff0000",
141 | "GraphQL": "#e10098",
142 | "Groovy": "#e69f56",
143 | "Groovy Server Pages": null,
144 | "Hack": "#878787",
145 | "Haml": "#ece2a9",
146 | "Handlebars": "#f7931e",
147 | "Harbour": "#0e60e3",
148 | "Haskell": "#5e5086",
149 | "Haxe": "#df7900",
150 | "HCL": null,
151 | "HiveQL": "#dce200",
152 | "HLSL": null,
153 | "HolyC": "#ffefaf",
154 | "HTML": "#e34c26",
155 | "Hy": "#7790B2",
156 | "HyPhy": null,
157 | "IDL": "#a3522f",
158 | "Idris": "#b30000",
159 | "IGOR Pro": "#0000cc",
160 | "Inform 7": null,
161 | "Inno Setup": null,
162 | "Io": "#a9188d",
163 | "Ioke": "#078193",
164 | "Isabelle": "#FEFE00",
165 | "Isabelle ROOT": null,
166 | "J": "#9EEDFF",
167 | "Jasmin": null,
168 | "Java": "#b07219",
169 | "Java Server Pages": null,
170 | "JavaScript": "#f1e05a",
171 | "JavaScript+ERB": null,
172 | "JFlex": "#DBCA00",
173 | "Jison": null,
174 | "Jison Lex": null,
175 | "Jolie": "#843179",
176 | "JSONiq": "#40d47e",
177 | "Jsonnet": "#0064bd",
178 | "JSX": null,
179 | "Julia": "#a270ba",
180 | "Jupyter Notebook": "#DA5B0B",
181 | "Kaitai Struct": "#773b37",
182 | "Kotlin": "#F18E33",
183 | "KRL": "#28430A",
184 | "LabVIEW": null,
185 | "Lasso": "#999999",
186 | "Latte": "#f2a542",
187 | "Lean": null,
188 | "Less": "#1d365d",
189 | "Lex": "#DBCA00",
190 | "LFE": "#4C3023",
191 | "LilyPond": null,
192 | "Limbo": null,
193 | "Literate Agda": null,
194 | "Literate CoffeeScript": null,
195 | "Literate Haskell": null,
196 | "LiveScript": "#499886",
197 | "LLVM": "#185619",
198 | "Logos": null,
199 | "Logtalk": null,
200 | "LOLCODE": "#cc9900",
201 | "LookML": "#652B81",
202 | "LoomScript": null,
203 | "LSL": "#3d9970",
204 | "Lua": "#000080",
205 | "M": null,
206 | "M4": null,
207 | "M4Sugar": null,
208 | "Macaulay2": "#d8ffff",
209 | "Makefile": "#427819",
210 | "Mako": null,
211 | "Markdown": "#083fa1",
212 | "Marko": "#42bff2",
213 | "Mask": "#f97732",
214 | "Mathematica": null,
215 | "MATLAB": "#e16737",
216 | "Max": "#c4a79c",
217 | "MAXScript": "#00a6a6",
218 | "mcfunction": "#E22837",
219 | "Mercury": "#ff2b2b",
220 | "Meson": "#007800",
221 | "Metal": "#8f14e9",
222 | "MiniD": null,
223 | "Mirah": "#c7a938",
224 | "mIRC Script": "#3d57c3",
225 | "MLIR": "#5EC8DB",
226 | "Modelica": null,
227 | "Modula-2": null,
228 | "Modula-3": "#223388",
229 | "Module Management System": null,
230 | "Monkey": null,
231 | "Moocode": null,
232 | "MoonScript": null,
233 | "Motorola 68K Assembly": null,
234 | "MQL4": "#62A8D6",
235 | "MQL5": "#4A76B8",
236 | "MTML": "#b7e1f4",
237 | "MUF": null,
238 | "mupad": null,
239 | "Myghty": null,
240 | "NASL": null,
241 | "NCL": "#28431f",
242 | "Nearley": "#990000",
243 | "Nemerle": "#3d3c6e",
244 | "nesC": "#94B0C7",
245 | "NetLinx": "#0aa0ff",
246 | "NetLinx+ERB": "#747faa",
247 | "NetLogo": "#ff6375",
248 | "NewLisp": "#87AED7",
249 | "Nextflow": "#3ac486",
250 | "Nim": "#ffc200",
251 | "Nit": "#009917",
252 | "Nix": "#7e7eff",
253 | "NSIS": null,
254 | "Nu": "#c9df40",
255 | "NumPy": "#9C8AF9",
256 | "Objective-C": "#438eff",
257 | "Objective-C++": "#6866fb",
258 | "Objective-J": "#ff0c5a",
259 | "ObjectScript": "#424893",
260 | "OCaml": "#3be133",
261 | "Odin": "#60AFFE",
262 | "Omgrofl": "#cabbff",
263 | "ooc": "#b0b77e",
264 | "Opa": null,
265 | "Opal": "#f7ede0",
266 | "Open Policy Agent": null,
267 | "OpenCL": null,
268 | "OpenEdge ABL": null,
269 | "OpenQASM": "#AA70FF",
270 | "OpenRC runscript": null,
271 | "OpenSCAD": null,
272 | "Ox": null,
273 | "Oxygene": "#cdd0e3",
274 | "Oz": "#fab738",
275 | "P4": "#7055b5",
276 | "Pan": "#cc0000",
277 | "Papyrus": "#6600cc",
278 | "Parrot": "#f3ca0a",
279 | "Parrot Assembly": null,
280 | "Parrot Internal Representation": null,
281 | "Pascal": "#E3F171",
282 | "Pawn": "#dbb284",
283 | "Pep8": "#C76F5B",
284 | "Perl": "#0298c3",
285 | "PHP": "#4F5D95",
286 | "PicoLisp": null,
287 | "PigLatin": "#fcd7de",
288 | "Pike": "#005390",
289 | "PLpgSQL": null,
290 | "PLSQL": "#dad8d8",
291 | "PogoScript": "#d80074",
292 | "Pony": null,
293 | "PostScript": "#da291c",
294 | "POV-Ray SDL": null,
295 | "PowerBuilder": "#8f0f8d",
296 | "PowerShell": "#012456",
297 | "Prisma": "#0c344b",
298 | "Processing": "#0096D8",
299 | "Prolog": "#74283c",
300 | "Propeller Spin": "#7fa2a7",
301 | "Pug": "#a86454",
302 | "Puppet": "#302B6D",
303 | "PureBasic": "#5a6986",
304 | "PureScript": "#1D222D",
305 | "Python": "#3572A5",
306 | "Python console": null,
307 | "q": "#0040cd",
308 | "Q#": "#fed659",
309 | "QMake": null,
310 | "QML": "#44a51c",
311 | "Qt Script": "#00b841",
312 | "Quake": "#882233",
313 | "R": "#198CE7",
314 | "Racket": "#3c5caa",
315 | "Ragel": "#9d5200",
316 | "Raku": "#0000fb",
317 | "RAML": "#77d9fb",
318 | "Rascal": "#fffaa0",
319 | "REALbasic": null,
320 | "Reason": "#ff5847",
321 | "Rebol": "#358a5b",
322 | "Red": "#f50000",
323 | "Redcode": null,
324 | "Ren'Py": "#ff7f7f",
325 | "RenderScript": null,
326 | "REXX": null,
327 | "Ring": "#2D54CB",
328 | "Riot": "#A71E49",
329 | "RobotFramework": null,
330 | "Roff": "#ecdebe",
331 | "Rouge": "#cc0088",
332 | "RPC": null,
333 | "Ruby": "#701516",
334 | "RUNOFF": "#665a4e",
335 | "Rust": "#dea584",
336 | "Sage": null,
337 | "SaltStack": "#646464",
338 | "SAS": "#B34936",
339 | "Sass": "#a53b70",
340 | "Scala": "#c22d40",
341 | "Scheme": "#1e4aec",
342 | "Scilab": null,
343 | "SCSS": "#c6538c",
344 | "sed": "#64b970",
345 | "Self": "#0579aa",
346 | "ShaderLab": null,
347 | "Shell": "#89e051",
348 | "ShellSession": null,
349 | "Shen": "#120F14",
350 | "Sieve": null,
351 | "Slash": "#007eff",
352 | "Slice": "#003fa2",
353 | "Slim": "#2b2b2b",
354 | "Smali": null,
355 | "Smalltalk": "#596706",
356 | "Smarty": null,
357 | "SmPL": "#c94949",
358 | "SMT": null,
359 | "Solidity": "#AA6746",
360 | "SourcePawn": "#f69e1d",
361 | "SQF": "#3F3F3F",
362 | "SQLPL": null,
363 | "Squirrel": "#800000",
364 | "SRecode Template": "#348a34",
365 | "Stan": "#b2011d",
366 | "Standard ML": "#dc566d",
367 | "Starlark": "#76d275",
368 | "Stata": null,
369 | "Stylus": "#ff6347",
370 | "SuperCollider": "#46390b",
371 | "Svelte": "#ff3e00",
372 | "SVG": "#ff9900",
373 | "Swift": "#ffac45",
374 | "SWIG": null,
375 | "SystemVerilog": "#DAE1C2",
376 | "Tcl": "#e4cc98",
377 | "Tcsh": null,
378 | "Terra": "#00004c",
379 | "TeX": "#3D6117",
380 | "Thrift": null,
381 | "TI Program": "#A0AA87",
382 | "TLA": null,
383 | "TSQL": null,
384 | "TSX": null,
385 | "Turing": "#cf142b",
386 | "Twig": "#c1d026",
387 | "TXL": null,
388 | "TypeScript": "#2b7489",
389 | "Unified Parallel C": "#4e3617",
390 | "Unix Assembly": null,
391 | "Uno": "#9933cc",
392 | "UnrealScript": "#a54c4d",
393 | "UrWeb": null,
394 | "V": "#4f87c4",
395 | "Vala": "#fbe5cd",
396 | "VBA": "#867db1",
397 | "VBScript": "#15dcdc",
398 | "VCL": "#148AA8",
399 | "Verilog": "#b2b7f8",
400 | "VHDL": "#adb2cb",
401 | "Vim script": "#199f4b",
402 | "Visual Basic .NET": "#945db7",
403 | "Volt": "#1F1F1F",
404 | "Vue": "#2c3e50",
405 | "wdl": "#42f1f4",
406 | "WebAssembly": "#04133b",
407 | "WebIDL": null,
408 | "wisp": "#7582D1",
409 | "Wollok": "#a23738",
410 | "X10": "#4B6BEF",
411 | "xBase": "#403a40",
412 | "XC": "#99DA07",
413 | "Xojo": null,
414 | "XProc": null,
415 | "XQuery": "#5232e7",
416 | "XS": null,
417 | "XSLT": "#EB8CEB",
418 | "Xtend": null,
419 | "Yacc": "#4B6C4B",
420 | "YAML": "#cb171e",
421 | "YARA": "#220000",
422 | "YASnippet": "#32AB90",
423 | "ZAP": "#0d665e",
424 | "Zeek": null,
425 | "ZenScript": "#00BCD1",
426 | "Zephir": "#118f9e",
427 | "Zig": "#ec915c",
428 | "ZIL": "#dc75e5",
429 | "Zimpl": null
430 | }
--------------------------------------------------------------------------------
/src/common/retryer.js:
--------------------------------------------------------------------------------
1 | const { logger, CustomError } = require("../common/utils");
2 |
3 | const retryer = async (fetcher, variables, retries = 0) => {
4 | if (retries > 7) {
5 | throw new CustomError("Maximum retries exceeded", CustomError.MAX_RETRY);
6 | }
7 | try {
8 | // try to fetch with the first token since RETRIES is 0 index i'm adding +1
9 | let response = await fetcher(
10 | variables,
11 | process.env[`PAT_${retries + 1}`],
12 | retries,
13 | );
14 |
15 | // prettier-ignore
16 | const isRateExceeded = response.data.errors && response.data.errors[0].type === "RATE_LIMITED";
17 |
18 | // if rate limit is hit increase the RETRIES and recursively call the retryer
19 | // with username, and current RETRIES
20 | if (isRateExceeded) {
21 | logger.log(`PAT_${retries + 1} Failed`);
22 | retries++;
23 | // directly return from the function
24 | return retryer(fetcher, variables, retries);
25 | }
26 |
27 | // finally return the response
28 | return response;
29 | } catch (err) {
30 | // prettier-ignore
31 | // also checking for bad credentials if any tokens gets invalidated
32 | const isBadCredential = err.response.data && err.response.data.message === "Bad credentials";
33 |
34 | if (isBadCredential) {
35 | logger.log(`PAT_${retries + 1} Failed`);
36 | retries++;
37 | // directly return from the function
38 | return retryer(fetcher, variables, retries);
39 | }
40 | }
41 | };
42 |
43 | module.exports = retryer;
44 |
--------------------------------------------------------------------------------
/src/common/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 |
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 isValidGradient(colors) {
64 | return isValidHexColor(colors[1]) && isValidHexColor(colors[2]);
65 | }
66 |
67 | function fallbackColor(color, fallbackColor) {
68 | let colors = color.split(",");
69 | let gradient = null;
70 |
71 | if (colors.length > 1 && isValidGradient(colors)) {
72 | gradient = colors;
73 | }
74 |
75 | return (
76 | (gradient ? gradient : isValidHexColor(color) && `#${color}`) ||
77 | fallbackColor
78 | );
79 | }
80 |
81 | function request(data, headers) {
82 | return axios({
83 | url: "https://api.github.com/graphql",
84 | method: "post",
85 | headers,
86 | data,
87 | });
88 | }
89 |
90 | /**
91 | *
92 | * @param {String[]} items
93 | * @param {Number} gap
94 | * @param {string} direction
95 | *
96 | * @description
97 | * Auto layout utility, allows us to layout things
98 | * vertically or horizontally with proper gaping
99 | */
100 | function FlexLayout({ items, gap, direction }) {
101 | // filter() for filtering out empty strings
102 | return items.filter(Boolean).map((item, i) => {
103 | let transform = `translate(${gap * i}, 0)`;
104 | if (direction === "column") {
105 | transform = `translate(0, ${gap * i})`;
106 | }
107 | return `${item}`;
108 | });
109 | }
110 |
111 | // returns theme based colors with proper overrides and defaults
112 | function getCardColors({
113 | title_color,
114 | text_color,
115 | icon_color,
116 | bg_color,
117 | theme,
118 | fallbackTheme = "default",
119 | }) {
120 | const defaultTheme = themes[fallbackTheme];
121 | const selectedTheme = themes[theme] || defaultTheme;
122 |
123 | // get the color provided by the user else the theme color
124 | // finally if both colors are invalid fallback to default theme
125 | const titleColor = fallbackColor(
126 | title_color || selectedTheme.title_color,
127 | "#" + defaultTheme.title_color,
128 | );
129 | const iconColor = fallbackColor(
130 | icon_color || selectedTheme.icon_color,
131 | "#" + defaultTheme.icon_color,
132 | );
133 | const textColor = fallbackColor(
134 | text_color || selectedTheme.text_color,
135 | "#" + defaultTheme.text_color,
136 | );
137 | const bgColor = fallbackColor(
138 | bg_color || selectedTheme.bg_color,
139 | "#" + defaultTheme.bg_color,
140 | );
141 |
142 | return { titleColor, iconColor, textColor, bgColor };
143 | }
144 |
145 | function wrapTextMultiline(text, width = 60, maxLines = 3) {
146 | const wrapped = wrap(encodeHTML(text), { width })
147 | .split("\n") // Split wrapped lines to get an array of lines
148 | .map((line) => line.trim()); // Remove leading and trailing whitespace of each line
149 |
150 | const lines = wrapped.slice(0, maxLines); // Only consider maxLines lines
151 |
152 | // Add "..." to the last line if the text exceeds maxLines
153 | if (wrapped.length > maxLines) {
154 | lines[maxLines - 1] += "...";
155 | }
156 |
157 | // Remove empty lines if text fits in less than maxLines lines
158 | const multiLineText = lines.filter(Boolean);
159 | return multiLineText;
160 | }
161 |
162 | const noop = () => {};
163 | // return console instance based on the environment
164 | const logger =
165 | process.env.NODE_ENV !== "test" ? console : { log: noop, error: noop };
166 |
167 | const CONSTANTS = {
168 | THIRTY_MINUTES: 1800,
169 | TWO_HOURS: 7200,
170 | FOUR_HOURS: 14400,
171 | ONE_DAY: 86400,
172 | };
173 |
174 | const SECONDARY_ERROR_MESSAGES = {
175 | MAX_RETRY:
176 | "Please add an env variable called PAT_1 with your github token in vercel",
177 | USER_NOT_FOUND: "Make sure the provided username is not an organization",
178 | };
179 |
180 | class CustomError extends Error {
181 | constructor(message, type) {
182 | super(message);
183 | this.type = type;
184 | this.secondaryMessage = SECONDARY_ERROR_MESSAGES[type] || "adsad";
185 | }
186 |
187 | static MAX_RETRY = "MAX_RETRY";
188 | static USER_NOT_FOUND = "USER_NOT_FOUND";
189 | }
190 |
191 | // https://stackoverflow.com/a/48172630/10629172
192 | function measureText(str, fontSize = 10) {
193 | // prettier-ignore
194 | const widths = [
195 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
196 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
197 | 0, 0, 0, 0, 0.2796875, 0.2765625,
198 | 0.3546875, 0.5546875, 0.5546875, 0.8890625, 0.665625, 0.190625,
199 | 0.3328125, 0.3328125, 0.3890625, 0.5828125, 0.2765625, 0.3328125,
200 | 0.2765625, 0.3015625, 0.5546875, 0.5546875, 0.5546875, 0.5546875,
201 | 0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.5546875,
202 | 0.2765625, 0.2765625, 0.584375, 0.5828125, 0.584375, 0.5546875,
203 | 1.0140625, 0.665625, 0.665625, 0.721875, 0.721875, 0.665625,
204 | 0.609375, 0.7765625, 0.721875, 0.2765625, 0.5, 0.665625,
205 | 0.5546875, 0.8328125, 0.721875, 0.7765625, 0.665625, 0.7765625,
206 | 0.721875, 0.665625, 0.609375, 0.721875, 0.665625, 0.94375,
207 | 0.665625, 0.665625, 0.609375, 0.2765625, 0.3546875, 0.2765625,
208 | 0.4765625, 0.5546875, 0.3328125, 0.5546875, 0.5546875, 0.5,
209 | 0.5546875, 0.5546875, 0.2765625, 0.5546875, 0.5546875, 0.221875,
210 | 0.240625, 0.5, 0.221875, 0.8328125, 0.5546875, 0.5546875,
211 | 0.5546875, 0.5546875, 0.3328125, 0.5, 0.2765625, 0.5546875,
212 | 0.5, 0.721875, 0.5, 0.5, 0.5, 0.3546875, 0.259375, 0.353125, 0.5890625,
213 | ];
214 |
215 | const avg = 0.5279276315789471;
216 | return (
217 | str
218 | .split("")
219 | .map((c) =>
220 | c.charCodeAt(0) < widths.length ? widths[c.charCodeAt(0)] : avg,
221 | )
222 | .reduce((cur, acc) => acc + cur) * fontSize
223 | );
224 | }
225 |
226 | module.exports = {
227 | renderError,
228 | kFormatter,
229 | encodeHTML,
230 | isValidHexColor,
231 | request,
232 | parseArray,
233 | parseBoolean,
234 | fallbackColor,
235 | FlexLayout,
236 | getCardColors,
237 | clampValue,
238 | wrapTextMultiline,
239 | measureText,
240 | logger,
241 | CONSTANTS,
242 | CustomError,
243 | };
244 |
--------------------------------------------------------------------------------
/src/fetchers/repo-fetcher.js:
--------------------------------------------------------------------------------
1 | const { request } = require("../common/utils");
2 | const retryer = require("../common/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/fetchers/stats-fetcher.js:
--------------------------------------------------------------------------------
1 | const { request, logger, CustomError } = require("../common/utils");
2 | const axios = require("axios");
3 | const retryer = require("../common/retryer");
4 | const calculateRank = require("../calculateRank");
5 | const githubUsernameRegex = require("github-username-regex");
6 |
7 | require("dotenv").config();
8 |
9 | const fetcher = (variables, token) => {
10 | return request(
11 | {
12 | query: `
13 | query userInfo($login: String!) {
14 | user(login: $login) {
15 | name
16 | login
17 | contributionsCollection {
18 | totalCommitContributions
19 | restrictedContributionsCount
20 | }
21 | repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
22 | totalCount
23 | }
24 | pullRequests(first: 1) {
25 | totalCount
26 | }
27 | issues(first: 1) {
28 | totalCount
29 | }
30 | followers {
31 | totalCount
32 | }
33 | repositories(first: 100, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}) {
34 | totalCount
35 | nodes {
36 | stargazers {
37 | totalCount
38 | }
39 | }
40 | }
41 | }
42 | }
43 | `,
44 | variables,
45 | },
46 | {
47 | Authorization: `bearer ${token}`,
48 | },
49 | );
50 | };
51 |
52 | // https://github.com/anuraghazra/github-readme-stats/issues/92#issuecomment-661026467
53 | // https://github.com/anuraghazra/github-readme-stats/pull/211/
54 | const totalCommitsFetcher = async (username) => {
55 | if (!githubUsernameRegex.test(username)) {
56 | logger.log("Invalid username");
57 | return 0;
58 | }
59 |
60 | // https://developer.github.com/v3/search/#search-commits
61 | const fetchTotalCommits = (variables, token) => {
62 | return axios({
63 | method: "get",
64 | url: `https://api.github.com/search/commits?q=author:${variables.login}`,
65 | headers: {
66 | "Content-Type": "application/json",
67 | Accept: "application/vnd.github.cloak-preview",
68 | Authorization: `bearer ${token}`,
69 | },
70 | });
71 | };
72 |
73 | try {
74 | let res = await retryer(fetchTotalCommits, { login: username });
75 | if (res.data.total_count) {
76 | return res.data.total_count;
77 | }
78 | } catch (err) {
79 | logger.log(err);
80 | // just return 0 if there is something wrong so that
81 | // we don't break the whole app
82 | return 0;
83 | }
84 | };
85 |
86 | async function fetchStats(
87 | username,
88 | count_private = false,
89 | include_all_commits = false,
90 | ) {
91 | if (!username) throw Error("Invalid username");
92 |
93 | const stats = {
94 | name: "",
95 | totalPRs: 0,
96 | totalCommits: 0,
97 | totalIssues: 0,
98 | totalStars: 0,
99 | contributedTo: 0,
100 | rank: { level: "C", score: 0 },
101 | };
102 |
103 | let res = await retryer(fetcher, { login: username });
104 |
105 | if (res.data.errors) {
106 | logger.error(res.data.errors);
107 | throw new CustomError(
108 | res.data.errors[0].message || "Could not fetch user",
109 | CustomError.USER_NOT_FOUND,
110 | );
111 | }
112 |
113 | const user = res.data.data.user;
114 |
115 | stats.name = user.name || user.login;
116 | stats.totalIssues = user.issues.totalCount;
117 |
118 | // normal commits
119 | stats.totalCommits = user.contributionsCollection.totalCommitContributions;
120 |
121 | // if include_all_commits then just get that,
122 | // since totalCommitsFetcher already sends totalCommits no need to +=
123 | if (include_all_commits) {
124 | stats.totalCommits = await totalCommitsFetcher(username);
125 | }
126 |
127 | // if count_private then add private commits to totalCommits so far.
128 | if (count_private) {
129 | stats.totalCommits +=
130 | user.contributionsCollection.restrictedContributionsCount;
131 | }
132 |
133 | stats.totalPRs = user.pullRequests.totalCount;
134 | stats.contributedTo = user.repositoriesContributedTo.totalCount;
135 |
136 | stats.totalStars = user.repositories.nodes.reduce((prev, curr) => {
137 | return prev + curr.stargazers.totalCount;
138 | }, 0);
139 |
140 | stats.rank = calculateRank({
141 | totalCommits: stats.totalCommits,
142 | totalRepos: user.repositories.totalCount,
143 | followers: user.followers.totalCount,
144 | contributions: stats.contributedTo,
145 | stargazers: stats.totalStars,
146 | prs: stats.totalPRs,
147 | issues: stats.totalIssues,
148 | });
149 |
150 | return stats;
151 | }
152 |
153 | module.exports = fetchStats;
154 |
--------------------------------------------------------------------------------
/src/fetchers/top-languages-fetcher.js:
--------------------------------------------------------------------------------
1 | const { request, logger, clampValue } = require("../common/utils");
2 | const retryer = require("../common/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 | name
15 | languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {
16 | edges {
17 | size
18 | node {
19 | color
20 | name
21 | }
22 | }
23 | }
24 | }
25 | }
26 | }
27 | }
28 | `,
29 | variables,
30 | },
31 | {
32 | Authorization: `bearer ${token}`,
33 | },
34 | );
35 | };
36 |
37 | async function fetchTopLanguages(username, langsCount = 5, exclude_repo = []) {
38 | if (!username) throw Error("Invalid username");
39 |
40 | langsCount = clampValue(parseInt(langsCount), 1, 10);
41 |
42 | const res = await retryer(fetcher, { login: username });
43 |
44 | if (res.data.errors) {
45 | logger.error(res.data.errors);
46 | throw Error(res.data.errors[0].message || "Could not fetch user");
47 | }
48 |
49 | let repoNodes = res.data.data.user.repositories.nodes;
50 | let repoToHide = {};
51 |
52 | // populate repoToHide map for quick lookup
53 | // while filtering out
54 | if (exclude_repo) {
55 | exclude_repo.forEach((repoName) => {
56 | repoToHide[repoName] = true;
57 | });
58 | }
59 |
60 | // filter out repositories to be hidden
61 | repoNodes = repoNodes
62 | .sort((a, b) => b.size - a.size)
63 | .filter((name) => {
64 | return !repoToHide[name.name];
65 | });
66 |
67 | repoNodes = repoNodes
68 | .filter((node) => {
69 | return node.languages.edges.length > 0;
70 | })
71 | // flatten the list of language nodes
72 | .reduce((acc, curr) => curr.languages.edges.concat(acc), [])
73 | .reduce((acc, prev) => {
74 | // get the size of the language (bytes)
75 | let langSize = prev.size;
76 |
77 | // if we already have the language in the accumulator
78 | // & the current language name is same as previous name
79 | // add the size to the language size.
80 | if (acc[prev.node.name] && prev.node.name === acc[prev.node.name].name) {
81 | langSize = prev.size + acc[prev.node.name].size;
82 | }
83 | return {
84 | ...acc,
85 | [prev.node.name]: {
86 | name: prev.node.name,
87 | color: prev.node.color,
88 | size: langSize,
89 | },
90 | };
91 | }, {});
92 |
93 | const topLangs = Object.keys(repoNodes)
94 | .sort((a, b) => repoNodes[b].size - repoNodes[a].size)
95 | .slice(0, langsCount)
96 | .reduce((result, key) => {
97 | result[key] = repoNodes[key];
98 | return result;
99 | }, {});
100 |
101 | return topLangs;
102 | }
103 |
104 | module.exports = fetchTopLanguages;
105 |
--------------------------------------------------------------------------------
/src/fetchers/wakatime-fetcher.js:
--------------------------------------------------------------------------------
1 | const axios = require("axios");
2 |
3 | const fetchWakatimeStats = async ({ username, api_domain, range }) => {
4 | try {
5 | const { data } = await axios.get(
6 | `https://${
7 | api_domain ? api_domain.replace(/[^a-z-.0-9]/gi, "") : "wakatime.com"
8 | }/api/v1/users/${username}/stats/${range || ''}?is_including_today=true`,
9 | );
10 |
11 | return data.data;
12 | } catch (err) {
13 | if (err.response.status < 200 || err.response.status > 299) {
14 | throw new Error(
15 | "Wakatime user not found, make sure you have a wakatime profile",
16 | );
17 | }
18 | throw err;
19 | }
20 | };
21 |
22 | module.exports = {
23 | fetchWakatimeStats,
24 | };
25 |
--------------------------------------------------------------------------------
/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 getProgressAnimation = ({ progress }) => {
13 | return `
14 | @keyframes rankAnimation {
15 | from {
16 | stroke-dashoffset: ${calculateCircleProgress(0)};
17 | }
18 | to {
19 | stroke-dashoffset: ${calculateCircleProgress(progress)};
20 | }
21 | }
22 | `;
23 | };
24 |
25 | const getAnimations = () => {
26 | return `
27 | /* Animations */
28 | @keyframes scaleInAnimation {
29 | from {
30 | transform: translate(-5px, 5px) scale(0);
31 | }
32 | to {
33 | transform: translate(-5px, 5px) scale(1);
34 | }
35 | }
36 | @keyframes fadeInAnimation {
37 | from {
38 | opacity: 0;
39 | }
40 | to {
41 | opacity: 1;
42 | }
43 | }
44 | `;
45 | };
46 |
47 | const getStyles = ({
48 | titleColor,
49 | textColor,
50 | iconColor,
51 | show_icons,
52 | progress,
53 | }) => {
54 | return `
55 | .stat {
56 | font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
57 | }
58 | .stagger {
59 | opacity: 0;
60 | animation: fadeInAnimation 0.3s ease-in-out forwards;
61 | }
62 | .rank-text {
63 | font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor};
64 | animation: scaleInAnimation 0.3s ease-in-out forwards;
65 | }
66 |
67 | .bold { font-weight: 700 }
68 | .icon {
69 | fill: ${iconColor};
70 | display: ${!!show_icons ? "block" : "none"};
71 | }
72 |
73 | .rank-circle-rim {
74 | stroke: ${titleColor};
75 | fill: none;
76 | stroke-width: 6;
77 | opacity: 0.2;
78 | }
79 | .rank-circle {
80 | stroke: ${titleColor};
81 | stroke-dasharray: 250;
82 | fill: none;
83 | stroke-width: 6;
84 | stroke-linecap: round;
85 | opacity: 0.8;
86 | transform-origin: -10px 8px;
87 | transform: rotate(-90deg);
88 | animation: rankAnimation 1s forwards ease-in-out;
89 | }
90 | ${process.env.NODE_ENV === "test" ? "" : getProgressAnimation({ progress })}
91 | `;
92 | };
93 |
94 | module.exports = { getStyles, getAnimations };
95 |
--------------------------------------------------------------------------------
/src/translations.js:
--------------------------------------------------------------------------------
1 | const { encodeHTML } = require("./common/utils");
2 |
3 | const statCardLocales = ({ name, apostrophe }) => {
4 | return {
5 | "statcard.title": {
6 | cn: `${encodeHTML(name)} 的 GitHub 统计数据`,
7 | cs: `GitHub statistiky uživatele ${encodeHTML(name)}`,
8 | de: `${encodeHTML(name) + apostrophe} GitHub-Statistiken`,
9 | en: `${encodeHTML(name)}'${apostrophe} GitHub Stats`,
10 | es: `Estadísticas de GitHub de ${encodeHTML(name)}`,
11 | fr: `Statistiques GitHub de ${encodeHTML(name)}`,
12 | hu: `${encodeHTML(name)} GitHub statisztika`,
13 | it: `Statistiche GitHub di ${encodeHTML(name)}`,
14 | ja: `${encodeHTML(name)}の GitHub 統計`,
15 | kr: `${encodeHTML(name)}의 GitHub 통계`,
16 | nl: `${encodeHTML(name)}'${apostrophe} GitHub Statistieken`,
17 | "pt-pt": `Estatísticas do GitHub de ${encodeHTML(name)}`,
18 | "pt-br": `Estatísticas do GitHub de ${encodeHTML(name)}`,
19 | np: `${encodeHTML(name)}'${apostrophe} गिटहब तथ्याङ्क`,
20 | el: `Στατιστικά GitHub του ${encodeHTML(name)}`,
21 | ru: `Статистика GitHub пользователя ${encodeHTML(name)}`,
22 | "uk-ua": `Статистика GitHub користувача ${encodeHTML(name)}`,
23 | id: `Statistik GitHub ${encodeHTML(name)}`,
24 | my: `Statistik GitHub ${encodeHTML(name)}`,
25 | sk: `GitHub štatistiky používateľa ${encodeHTML(name)}`,
26 | tr: `${encodeHTML(name)} Hesabının GitHub Yıldızları`,
27 | pl: `Statystyki GitHub użytkownika ${encodeHTML(name)}`,
28 | },
29 | "statcard.totalstars": {
30 | cn: "获标星数(star)",
31 | cs: "Celkem hvězd",
32 | de: "Sterne Insgesamt",
33 | en: "Total Stars",
34 | es: "Estrellas totales",
35 | fr: "Total d'étoiles",
36 | hu: "Csillagok",
37 | it: "Stelle totali",
38 | ja: "スターされた数",
39 | kr: "받은 스타 수",
40 | nl: "Totale Sterren",
41 | "pt-pt": "Total de estrelas",
42 | "pt-br": "Total de estrelas",
43 | np: "कुल ताराहरू",
44 | el: "Σύνολο Αστεριών",
45 | ru: "Всего звезд",
46 | "uk-ua": "Всього зірок",
47 | id: "Total Bintang",
48 | my: "Jumlah Bintang",
49 | sk: "Hviezdy",
50 | tr: "Toplam Yıldız",
51 | pl: "Liczba Gwiazdek",
52 | },
53 | "statcard.commits": {
54 | cn: "累计提交数(commit)",
55 | cs: "Celkem commitů",
56 | de: "Anzahl Commits",
57 | en: "Total Commits",
58 | es: "Commits totales",
59 | fr: "Total des validations",
60 | hu: "Összes commit",
61 | it: "Commit totali",
62 | ja: "合計コミット数",
63 | kr: "전체 커밋 수",
64 | nl: "Totale Commits",
65 | "pt-pt": "Total de Commits",
66 | "pt-br": "Total de Commits",
67 | np: "कुल Commits",
68 | el: "Σύνολο Commits",
69 | ru: "Всего коммитов",
70 | "uk-ua": "Всього коммітов",
71 | id: "Total Komitmen",
72 | my: "Jumlah Komitmen",
73 | sk: "Všetky commity",
74 | tr: "Toplam Commit",
75 | pl: "Wszystkie commity",
76 | },
77 | "statcard.prs": {
78 | cn: "拉取请求数(PR)",
79 | cs: "Celkem PRs",
80 | de: "PRs Insgesamt",
81 | en: "Total PRs",
82 | es: "PRs totales",
83 | fr: "Total des PR",
84 | hu: "Összes PR",
85 | it: "PR totali",
86 | ja: "合計 PR",
87 | kr: "PR 횟수",
88 | nl: "Totale PR's",
89 | "pt-pt": "Total de PRs",
90 | "pt-br": "Total de PRs",
91 | np: "कुल PRs",
92 | el: "Σύνολο PRs",
93 | ru: "Всего pull request`ов",
94 | "uk-ua": "Всього pull request`iв",
95 | id: "Total Permintaan Tarik",
96 | my: "Jumlah PR",
97 | sk: "Všetky PR",
98 | tr: "Toplam PR",
99 | pl: "Wszystkie PR",
100 | },
101 | "statcard.issues": {
102 | cn: "指出问题数(issue)",
103 | cs: "Celkem problémů",
104 | de: "Anzahl Issues",
105 | en: "Total Issues",
106 | es: "Issues totales",
107 | fr: "Nombre total d'incidents",
108 | hu: "Összes hibajegy",
109 | it: "Segnalazioni totali",
110 | ja: "合計 issue",
111 | kr: "이슈 개수",
112 | nl: "Totale Issues",
113 | "pt-pt": "Total de Issues",
114 | "pt-br": "Total de Issues",
115 | np: "कुल मुद्दाहरू",
116 | el: "Σύνολο Ζητημάτων",
117 | ru: "Всего issue",
118 | "uk-ua": "Всього issue",
119 | id: "Total Masalah Dilaporkan",
120 | my: "Jumlah Isu Dilaporkan",
121 | sk: "Všetky problémy",
122 | tr: "Toplam Hata",
123 | pl: "Wszystkie problemy",
124 | },
125 | "statcard.contribs": {
126 | cn: "参与项目数",
127 | cs: "Přispěl k",
128 | de: "Beigetragen zu",
129 | en: "Contributed to",
130 | es: "Contribuciones en",
131 | fr: "Contribué à",
132 | hu: "Hozzájárulások",
133 | it: "Ha contribuito a",
134 | ja: "コントリビュートしたリポジトリ",
135 | kr: "전체 기여도",
136 | nl: "Bijgedragen aan",
137 | "pt-pt": "Contribuiu em",
138 | "pt-br": "Contribuiu para",
139 | np: "कुल योगदानहरू",
140 | el: "Συνεισφέρθηκε σε",
141 | ru: "Внёс вклад в",
142 | "uk-ua": "Вніс внесок у",
143 | id: "Berkontribusi ke",
144 | my: "Menyumbang kepada",
145 | sk: "Účasti",
146 | tr: "Katkı Verildi",
147 | pl: "Udziały",
148 | },
149 | };
150 | };
151 |
152 | const repoCardLocales = {
153 | "repocard.template": {
154 | cn: "模板",
155 | cs: "Šablona",
156 | de: "Vorlage",
157 | en: "Template",
158 | es: "Planitlla",
159 | fr: "Modèle",
160 | hu: "Sablon",
161 | it: "Template",
162 | ja: "テンプレート",
163 | kr: "템플릿",
164 | nl: "Sjabloon",
165 | "pt-pt": "Modelo",
166 | "pt-br": "Modelo",
167 | np: "टेम्पलेट",
168 | el: "Πρότυπο",
169 | ru: "Шаблон",
170 | "uk-ua": "Шаблон",
171 | id: "Pola",
172 | my: "Templat",
173 | sk: "Šablóna",
174 | tr: "Şablon",
175 | pl: "Szablony",
176 | },
177 | "repocard.archived": {
178 | cn: "已归档",
179 | cs: "Archivováno",
180 | de: "Archiviert",
181 | en: "Archived",
182 | es: "Archivados",
183 | fr: "Archivé",
184 | hu: "Archivált",
185 | it: "Archiviata",
186 | ja: "アーカイブ済み",
187 | kr: "보관됨",
188 | nl: "Gearchiveerd",
189 | "pt-pt": "Arquivados",
190 | "pt-br": "Arquivados",
191 | np: "अभिलेख राखियो",
192 | el: "Αρχειοθετημένα",
193 | ru: "Архивирован",
194 | "uk-ua": "Архивирован",
195 | id: "Arsip",
196 | my: "Arkib",
197 | sk: "Archivované",
198 | tr: "Arşiv",
199 | pl: "Zarchiwizowano",
200 | },
201 | };
202 |
203 | const langCardLocales = {
204 | "langcard.title": {
205 | cn: "最常用的语言",
206 | cs: "Nejpoužívanější jazyky",
207 | de: "Meist verwendete Sprachen",
208 | en: "Most Used Languages",
209 | es: "Lenguajes más usados",
210 | fr: "Langages les plus utilisés",
211 | hu: "Leggyakrabban használt nyelvek",
212 | it: "Linguaggi più utilizzati",
213 | ja: "最もよく使っている言語",
214 | kr: "가장 많이 사용된 언어",
215 | nl: "Meest gebruikte talen",
216 | "pt-pt": "Idiomas mais usados",
217 | "pt-br": "Linguagens mais usadas",
218 | np: "अधिक प्रयोग गरिएको भाषाहरू",
219 | el: "Οι περισσότερο χρησιμοποιούμενες γλώσσες",
220 | ru: "Наиболее часто используемые языки",
221 | "uk-ua": "Найбільш часто використовувані мови",
222 | id: "Bahasa Yang Paling Banyak Digunakan",
223 | my: "Bahasa Paling Digunakan",
224 | sk: "Najviac používané jazyky",
225 | tr: "En Çok Kullanılan Diller",
226 | pl: "Najczęściej używane języki",
227 | },
228 | };
229 |
230 | const wakatimeCardLocales = {
231 | "wakatimecard.title": {
232 | cn: "Wakatime 周统计",
233 | cs: "Statistiky Wakatime",
234 | de: "Wakatime Status",
235 | en: "Wakatime Stats",
236 | es: "Estadísticas de Wakatime",
237 | fr: "Statistiques de Wakatime",
238 | hu: "Wakatime statisztika",
239 | it: "Statistiche Wakatime",
240 | ja: "Wakatime ワカタイム統計",
241 | kr: "Wakatime 주간 통계",
242 | nl: "Takwimu za Wakatime",
243 | "pt-pt": "Estatísticas Wakatime",
244 | "pt-br": "Estatísticas Wakatime",
245 | np: "Wakatime तथ्या .्क",
246 | el: "Στατιστικά Wakatime",
247 | ru: "Статистика Wakatime",
248 | "uk-ua": "Статистика Wakatime",
249 | id: "Status Wakatime",
250 | my: "Statistik Wakatime",
251 | sk: "Wakatime štatistika",
252 | tr: "Waketime İstatistikler",
253 | pl: "statystyki Wakatime",
254 | },
255 | "wakatimecard.nocodingactivity": {
256 | cn: "本周没有编程活动",
257 | cs: "Tento týden žádná aktivita v kódování",
258 | de: "Keine Aktivitäten in dieser Woche",
259 | en: "No coding activity this week",
260 | es: "No hay actividad de codificación esta semana",
261 | fr: "Aucune activité de codage cette semaine",
262 | hu: "Nem volt aktivitás ezen a héten",
263 | it: "Nessuna attività in questa settimana",
264 | ja: "今週のコーディング活動はありません",
265 | kr: "이번 주 작업내역 없음",
266 | nl: "Geen coderings activiet deze week",
267 | "pt-pt": "Sem atividade esta semana",
268 | "pt-br": "Nenhuma atividade de codificação esta semana",
269 | np: "यस हप्ता कुनै कोडिंग गतिविधि छैन",
270 | el: "Δεν υπάρχει δραστηριότητα κώδικα γι' αυτή την εβδομάδα",
271 | ru: "На этой неделе не было активности",
272 | "uk-ua": "На цьому тижні не було активності",
273 | id: "Tidak ada aktivitas perkodingan minggu ini",
274 | my: "Tiada aktiviti pengekodan minggu ini",
275 | sk: "Žiadna kódovacia aktivita tento týždeň",
276 | tr: "Bu hafta herhangi bir kod yazma aktivitesi olmadı",
277 | pl: "Brak aktywności w tym tygodniu",
278 | },
279 | };
280 |
281 | const availableLocales = Object.keys(repoCardLocales["repocard.archived"]);
282 |
283 | function isLocaleAvailable(locale) {
284 | return availableLocales.includes(locale.toLowerCase());
285 | }
286 |
287 | module.exports = {
288 | isLocaleAvailable,
289 | availableLocales,
290 | statCardLocales,
291 | repoCardLocales,
292 | langCardLocales,
293 | wakatimeCardLocales,
294 | };
295 |
--------------------------------------------------------------------------------
/tests/__snapshots__/renderWakatimeCard.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Test Render Wakatime Card should render correctly 1`] = `
4 | "
5 |
151 | "
152 | `;
153 |
154 | exports[`Test Render Wakatime Card should render correctly with compact layout 1`] = `
155 | "
156 |
163 |
214 |
215 | undefined
216 |
217 |
228 |
229 |
230 |
234 |
235 |
241 |
242 |
243 |
244 |
245 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
265 |
266 |
275 |
276 |
285 |
286 |
287 |
288 |
289 |
290 | Other - 19 mins
291 |
292 |
293 |
294 |
295 |
296 |
297 | TypeScript - 1 min
298 |
299 |
300 |
301 |
302 |
303 |
304 | YAML - 0 secs
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 | "
314 | `;
315 |
--------------------------------------------------------------------------------
/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/cards/stats-card");
6 | const { renderError, CONSTANTS } = require("../src/common/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.TWO_HOURS}`],
147 | ]);
148 | });
149 |
150 | it("should set proper cache", async () => {
151 | const { req, res } = faker({ cache_seconds: 8000 }, 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=${8000}`],
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.TWO_HOURS}`],
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.TWO_HOURS}`],
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/card.test.js:
--------------------------------------------------------------------------------
1 | require("@testing-library/jest-dom");
2 | const cssToObject = require("css-to-object");
3 | const Card = require("../src/common/Card");
4 | const icons = require("../src/common/icons");
5 | const { getCardColors } = require("../src/common/utils");
6 | const { queryByTestId } = require("@testing-library/dom");
7 |
8 | describe("Card", () => {
9 | it("should hide border", () => {
10 | const card = new Card({});
11 | card.setHideBorder(true);
12 |
13 | document.body.innerHTML = card.render(``);
14 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
15 | "stroke-opacity",
16 | "0",
17 | );
18 | });
19 |
20 | it("should not hide border", () => {
21 | const card = new Card({});
22 | card.setHideBorder(false);
23 |
24 | document.body.innerHTML = card.render(``);
25 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
26 | "stroke-opacity",
27 | "1",
28 | );
29 | });
30 |
31 | it("should have a custom title", () => {
32 | const card = new Card({
33 | customTitle: "custom title",
34 | defaultTitle: "default title",
35 | });
36 |
37 | document.body.innerHTML = card.render(``);
38 | expect(queryByTestId(document.body, "card-title")).toHaveTextContent(
39 | "custom title",
40 | );
41 | });
42 |
43 | it("should hide title", () => {
44 | const card = new Card({});
45 | card.setHideTitle(true);
46 |
47 | document.body.innerHTML = card.render(``);
48 | expect(queryByTestId(document.body, "card-title")).toBeNull();
49 | });
50 |
51 | it("should not hide title", () => {
52 | const card = new Card({});
53 | card.setHideTitle(false);
54 |
55 | document.body.innerHTML = card.render(``);
56 | expect(queryByTestId(document.body, "card-title")).toBeInTheDocument();
57 | });
58 |
59 | it("title should have prefix icon", () => {
60 | const card = new Card({ title: "ok", titlePrefixIcon: icons.contribs });
61 |
62 | document.body.innerHTML = card.render(``);
63 | expect(document.getElementsByClassName("icon")[0]).toBeInTheDocument();
64 | });
65 |
66 | it("title should not have prefix icon", () => {
67 | const card = new Card({ title: "ok" });
68 |
69 | document.body.innerHTML = card.render(``);
70 | expect(document.getElementsByClassName("icon")[0]).toBeUndefined();
71 | });
72 |
73 | it("should have proper height, width", () => {
74 | const card = new Card({ height: 200, width: 200, title: "ok" });
75 | document.body.innerHTML = card.render(``);
76 | expect(document.getElementsByTagName("svg")[0]).toHaveAttribute(
77 | "height",
78 | "200",
79 | );
80 | expect(document.getElementsByTagName("svg")[0]).toHaveAttribute(
81 | "height",
82 | "200",
83 | );
84 | });
85 |
86 | it("should have less height after title is hidden", () => {
87 | const card = new Card({ height: 200, title: "ok" });
88 | card.setHideTitle(true);
89 |
90 | document.body.innerHTML = card.render(``);
91 | expect(document.getElementsByTagName("svg")[0]).toHaveAttribute(
92 | "height",
93 | "170",
94 | );
95 | });
96 |
97 | it("main-card-body should have proper when title is visible", () => {
98 | const card = new Card({ height: 200 });
99 | document.body.innerHTML = card.render(``);
100 | expect(queryByTestId(document.body, "main-card-body")).toHaveAttribute(
101 | "transform",
102 | "translate(0, 55)",
103 | );
104 | });
105 |
106 | it("main-card-body should have proper position after title is hidden", () => {
107 | const card = new Card({ height: 200 });
108 | card.setHideTitle(true);
109 |
110 | document.body.innerHTML = card.render(``);
111 | expect(queryByTestId(document.body, "main-card-body")).toHaveAttribute(
112 | "transform",
113 | "translate(0, 25)",
114 | );
115 | });
116 |
117 | it("should render with correct colors", () => {
118 | // returns theme based colors with proper overrides and defaults
119 | const { titleColor, textColor, iconColor, bgColor } = getCardColors({
120 | title_color: "f00",
121 | icon_color: "0f0",
122 | text_color: "00f",
123 | bg_color: "fff",
124 | theme: "default",
125 | });
126 |
127 | const card = new Card({
128 | height: 200,
129 | colors: {
130 | titleColor,
131 | textColor,
132 | iconColor,
133 | bgColor,
134 | },
135 | });
136 | document.body.innerHTML = card.render(``);
137 |
138 | const styleTag = document.querySelector("style");
139 | const stylesObject = cssToObject(styleTag.innerHTML);
140 | const headerClassStyles = stylesObject[".header"];
141 |
142 | expect(headerClassStyles.fill).toBe("#f00");
143 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
144 | "fill",
145 | "#fff",
146 | );
147 | });
148 | it("should render gradient backgrounds", () => {
149 | const { titleColor, textColor, iconColor, bgColor } = getCardColors({
150 | title_color: "f00",
151 | icon_color: "0f0",
152 | text_color: "00f",
153 | bg_color: "90,fff,000,f00",
154 | theme: "default",
155 | });
156 |
157 | const card = new Card({
158 | height: 200,
159 | colors: {
160 | titleColor,
161 | textColor,
162 | iconColor,
163 | bgColor,
164 | },
165 | });
166 | document.body.innerHTML = card.render(``);
167 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
168 | "fill",
169 | "url(#gradient)",
170 | );
171 | expect(document.querySelector("defs linearGradient")).toHaveAttribute(
172 | "gradientTransform",
173 | "rotate(90)",
174 | );
175 | expect(
176 | document.querySelector("defs linearGradient stop:nth-child(1)"),
177 | ).toHaveAttribute("stop-color", "#fff");
178 | expect(
179 | document.querySelector("defs linearGradient stop:nth-child(2)"),
180 | ).toHaveAttribute("stop-color", "#000");
181 | expect(
182 | document.querySelector("defs linearGradient stop:nth-child(3)"),
183 | ).toHaveAttribute("stop-color", "#f00");
184 | });
185 | });
186 |
--------------------------------------------------------------------------------
/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/fetchers/repo-fetcher");
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/fetchers/stats-fetcher");
5 | const calculateRank = require("../src/calculateRank");
6 |
7 | const data = {
8 | data: {
9 | user: {
10 | name: "Anurag Hazra",
11 | repositoriesContributedTo: { totalCount: 61 },
12 | contributionsCollection: {
13 | totalCommitContributions: 100,
14 | restrictedContributionsCount: 50,
15 | },
16 | pullRequests: { totalCount: 300 },
17 | issues: { totalCount: 200 },
18 | followers: { totalCount: 100 },
19 | repositories: {
20 | totalCount: 5,
21 | nodes: [
22 | { stargazers: { totalCount: 100 } },
23 | { stargazers: { totalCount: 100 } },
24 | { stargazers: { totalCount: 100 } },
25 | { stargazers: { totalCount: 50 } },
26 | { stargazers: { totalCount: 50 } },
27 | ],
28 | },
29 | },
30 | },
31 | };
32 |
33 | const error = {
34 | errors: [
35 | {
36 | type: "NOT_FOUND",
37 | path: ["user"],
38 | locations: [],
39 | message: "Could not resolve to a User with the login of 'noname'.",
40 | },
41 | ],
42 | };
43 |
44 | const mock = new MockAdapter(axios);
45 |
46 | afterEach(() => {
47 | mock.reset();
48 | });
49 |
50 | describe("Test fetchStats", () => {
51 | it("should fetch correct stats", async () => {
52 | mock.onPost("https://api.github.com/graphql").reply(200, data);
53 |
54 | let stats = await fetchStats("anuraghazra");
55 | const rank = calculateRank({
56 | totalCommits: 100,
57 | totalRepos: 5,
58 | followers: 100,
59 | contributions: 61,
60 | stargazers: 400,
61 | prs: 300,
62 | issues: 200,
63 | });
64 |
65 | expect(stats).toStrictEqual({
66 | contributedTo: 61,
67 | name: "Anurag Hazra",
68 | totalCommits: 100,
69 | totalIssues: 200,
70 | totalPRs: 300,
71 | totalStars: 400,
72 | rank,
73 | });
74 | });
75 |
76 | it("should throw error", async () => {
77 | mock.onPost("https://api.github.com/graphql").reply(200, error);
78 |
79 | await expect(fetchStats("anuraghazra")).rejects.toThrow(
80 | "Could not resolve to a User with the login of 'noname'.",
81 | );
82 | });
83 |
84 | it("should fetch and add private contributions", async () => {
85 | mock.onPost("https://api.github.com/graphql").reply(200, data);
86 |
87 | let stats = await fetchStats("anuraghazra", true);
88 | const rank = calculateRank({
89 | totalCommits: 150,
90 | totalRepos: 5,
91 | followers: 100,
92 | contributions: 61,
93 | stargazers: 400,
94 | prs: 300,
95 | issues: 200,
96 | });
97 |
98 | expect(stats).toStrictEqual({
99 | contributedTo: 61,
100 | name: "Anurag Hazra",
101 | totalCommits: 150,
102 | totalIssues: 200,
103 | totalPRs: 300,
104 | totalStars: 400,
105 | rank,
106 | });
107 | });
108 |
109 | it("should fetch total commits", async () => {
110 | mock.onPost("https://api.github.com/graphql").reply(200, data);
111 | mock
112 | .onGet("https://api.github.com/search/commits?q=author:anuraghazra")
113 | .reply(200, { total_count: 1000 });
114 |
115 | let stats = await fetchStats("anuraghazra", true, true);
116 | const rank = calculateRank({
117 | totalCommits: 1050,
118 | totalRepos: 5,
119 | followers: 100,
120 | contributions: 61,
121 | stargazers: 400,
122 | prs: 300,
123 | issues: 200,
124 | });
125 |
126 | expect(stats).toStrictEqual({
127 | contributedTo: 61,
128 | name: "Anurag Hazra",
129 | totalCommits: 1050,
130 | totalIssues: 200,
131 | totalPRs: 300,
132 | totalStars: 400,
133 | rank,
134 | });
135 | });
136 | });
137 |
--------------------------------------------------------------------------------
/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/fetchers/top-languages-fetcher");
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 fetch langs with specified langs_count", async () => {
78 | mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
79 |
80 | let repo = await fetchTopLanguages("anuraghazra", 1);
81 | expect(repo).toStrictEqual({
82 | javascript: {
83 | color: "#0ff",
84 | name: "javascript",
85 | size: 200,
86 | },
87 | });
88 | });
89 |
90 | it("should throw error", async () => {
91 | mock.onPost("https://api.github.com/graphql").reply(200, error);
92 |
93 | await expect(fetchTopLanguages("anuraghazra")).rejects.toThrow(
94 | "Could not resolve to a User with the login of 'noname'.",
95 | );
96 | });
97 | });
98 |
--------------------------------------------------------------------------------
/tests/fetchWakatime.test.js:
--------------------------------------------------------------------------------
1 | require("@testing-library/jest-dom");
2 | const axios = require("axios");
3 | const MockAdapter = require("axios-mock-adapter");
4 | const { fetchWakatimeStats } = require("../src/fetchers/wakatime-fetcher");
5 | const mock = new MockAdapter(axios);
6 |
7 | afterEach(() => {
8 | mock.reset();
9 | });
10 |
11 | const wakaTimeData = {
12 | data: {
13 | categories: [
14 | {
15 | digital: "22:40",
16 | hours: 22,
17 | minutes: 40,
18 | name: "Coding",
19 | percent: 100,
20 | text: "22 hrs 40 mins",
21 | total_seconds: 81643.570077,
22 | },
23 | ],
24 | daily_average: 16095,
25 | daily_average_including_other_language: 16329,
26 | days_including_holidays: 7,
27 | days_minus_holidays: 5,
28 | editors: [
29 | {
30 | digital: "22:40",
31 | hours: 22,
32 | minutes: 40,
33 | name: "VS Code",
34 | percent: 100,
35 | text: "22 hrs 40 mins",
36 | total_seconds: 81643.570077,
37 | },
38 | ],
39 | holidays: 2,
40 | human_readable_daily_average: "4 hrs 28 mins",
41 | human_readable_daily_average_including_other_language: "4 hrs 32 mins",
42 | human_readable_total: "22 hrs 21 mins",
43 | human_readable_total_including_other_language: "22 hrs 40 mins",
44 | id: "random hash",
45 | is_already_updating: false,
46 | is_coding_activity_visible: true,
47 | is_including_today: false,
48 | is_other_usage_visible: true,
49 | is_stuck: false,
50 | is_up_to_date: true,
51 | languages: [
52 | {
53 | digital: "0:19",
54 | hours: 0,
55 | minutes: 19,
56 | name: "Other",
57 | percent: 1.43,
58 | text: "19 mins",
59 | total_seconds: 1170.434361,
60 | },
61 | {
62 | digital: "0:01",
63 | hours: 0,
64 | minutes: 1,
65 | name: "TypeScript",
66 | percent: 0.1,
67 | text: "1 min",
68 | total_seconds: 83.293809,
69 | },
70 | {
71 | digital: "0:00",
72 | hours: 0,
73 | minutes: 0,
74 | name: "YAML",
75 | percent: 0.07,
76 | text: "0 secs",
77 | total_seconds: 54.975151,
78 | },
79 | ],
80 | operating_systems: [
81 | {
82 | digital: "22:40",
83 | hours: 22,
84 | minutes: 40,
85 | name: "Mac",
86 | percent: 100,
87 | text: "22 hrs 40 mins",
88 | total_seconds: 81643.570077,
89 | },
90 | ],
91 | percent_calculated: 100,
92 | range: "last_7_days",
93 | status: "ok",
94 | timeout: 15,
95 | total_seconds: 80473.135716,
96 | total_seconds_including_other_language: 81643.570077,
97 | user_id: "random hash",
98 | username: "anuraghazra",
99 | writes_only: false,
100 | },
101 | };
102 |
103 | describe("Wakatime fetcher", () => {
104 | it("should fetch correct wakatime data", async () => {
105 | const username = "anuraghazra";
106 | mock
107 | .onGet(
108 | `https://wakatime.com/api/v1/users/${username}/stats/?is_including_today=true`,
109 | )
110 | .reply(200, wakaTimeData);
111 |
112 | const repo = await fetchWakatimeStats({ username });
113 | expect(repo).toMatchInlineSnapshot(`
114 | Object {
115 | "categories": Array [
116 | Object {
117 | "digital": "22:40",
118 | "hours": 22,
119 | "minutes": 40,
120 | "name": "Coding",
121 | "percent": 100,
122 | "text": "22 hrs 40 mins",
123 | "total_seconds": 81643.570077,
124 | },
125 | ],
126 | "daily_average": 16095,
127 | "daily_average_including_other_language": 16329,
128 | "days_including_holidays": 7,
129 | "days_minus_holidays": 5,
130 | "editors": Array [
131 | Object {
132 | "digital": "22:40",
133 | "hours": 22,
134 | "minutes": 40,
135 | "name": "VS Code",
136 | "percent": 100,
137 | "text": "22 hrs 40 mins",
138 | "total_seconds": 81643.570077,
139 | },
140 | ],
141 | "holidays": 2,
142 | "human_readable_daily_average": "4 hrs 28 mins",
143 | "human_readable_daily_average_including_other_language": "4 hrs 32 mins",
144 | "human_readable_total": "22 hrs 21 mins",
145 | "human_readable_total_including_other_language": "22 hrs 40 mins",
146 | "id": "random hash",
147 | "is_already_updating": false,
148 | "is_coding_activity_visible": true,
149 | "is_including_today": false,
150 | "is_other_usage_visible": true,
151 | "is_stuck": false,
152 | "is_up_to_date": true,
153 | "languages": Array [
154 | Object {
155 | "digital": "0:19",
156 | "hours": 0,
157 | "minutes": 19,
158 | "name": "Other",
159 | "percent": 1.43,
160 | "text": "19 mins",
161 | "total_seconds": 1170.434361,
162 | },
163 | Object {
164 | "digital": "0:01",
165 | "hours": 0,
166 | "minutes": 1,
167 | "name": "TypeScript",
168 | "percent": 0.1,
169 | "text": "1 min",
170 | "total_seconds": 83.293809,
171 | },
172 | Object {
173 | "digital": "0:00",
174 | "hours": 0,
175 | "minutes": 0,
176 | "name": "YAML",
177 | "percent": 0.07,
178 | "text": "0 secs",
179 | "total_seconds": 54.975151,
180 | },
181 | ],
182 | "operating_systems": Array [
183 | Object {
184 | "digital": "22:40",
185 | "hours": 22,
186 | "minutes": 40,
187 | "name": "Mac",
188 | "percent": 100,
189 | "text": "22 hrs 40 mins",
190 | "total_seconds": 81643.570077,
191 | },
192 | ],
193 | "percent_calculated": 100,
194 | "range": "last_7_days",
195 | "status": "ok",
196 | "timeout": 15,
197 | "total_seconds": 80473.135716,
198 | "total_seconds_including_other_language": 81643.570077,
199 | "user_id": "random hash",
200 | "username": "anuraghazra",
201 | "writes_only": false,
202 | }
203 | `);
204 | });
205 |
206 | it("should throw error", async () => {
207 | mock.onGet(/\/https:\/\/wakatime\.com\/api/).reply(404, wakaTimeData);
208 |
209 | await expect(fetchWakatimeStats("noone")).rejects.toThrow(
210 | "Wakatime user not found, make sure you have a wakatime profile",
211 | );
212 | });
213 | });
214 |
215 | module.exports = { wakaTimeData };
216 |
--------------------------------------------------------------------------------
/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/cards/repo-card");
6 | const { renderError } = require("../src/common/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/renderStatsCard.test.js:
--------------------------------------------------------------------------------
1 | require("@testing-library/jest-dom");
2 | const cssToObject = require("css-to-object");
3 | const renderStatsCard = require("../src/cards/stats-card");
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_rank", () => {
73 | document.body.innerHTML = renderStatsCard(stats, { hide_rank: true });
74 |
75 | expect(queryByTestId(document.body, "rank-circle")).not.toBeInTheDocument();
76 | });
77 |
78 | it("should render default colors properly", () => {
79 | document.body.innerHTML = renderStatsCard(stats);
80 |
81 | const styleTag = document.querySelector("style");
82 | const stylesObject = cssToObject(styleTag.textContent);
83 |
84 | const headerClassStyles = stylesObject[".header"];
85 | const statClassStyles = stylesObject[".stat"];
86 | const iconClassStyles = stylesObject[".icon"];
87 |
88 | expect(headerClassStyles.fill).toBe("#2f80ed");
89 | expect(statClassStyles.fill).toBe("#333");
90 | expect(iconClassStyles.fill).toBe("#4c71f2");
91 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
92 | "fill",
93 | "#fffefe",
94 | );
95 | });
96 |
97 | it("should render custom colors properly", () => {
98 | const customColors = {
99 | title_color: "5a0",
100 | icon_color: "1b998b",
101 | text_color: "9991",
102 | bg_color: "252525",
103 | };
104 |
105 | document.body.innerHTML = renderStatsCard(stats, { ...customColors });
106 |
107 | const styleTag = document.querySelector("style");
108 | const stylesObject = cssToObject(styleTag.innerHTML);
109 |
110 | const headerClassStyles = stylesObject[".header"];
111 | const statClassStyles = stylesObject[".stat"];
112 | const iconClassStyles = stylesObject[".icon"];
113 |
114 | expect(headerClassStyles.fill).toBe(`#${customColors.title_color}`);
115 | expect(statClassStyles.fill).toBe(`#${customColors.text_color}`);
116 | expect(iconClassStyles.fill).toBe(`#${customColors.icon_color}`);
117 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
118 | "fill",
119 | "#252525",
120 | );
121 | });
122 |
123 | it("should render custom colors with themes", () => {
124 | document.body.innerHTML = renderStatsCard(stats, {
125 | title_color: "5a0",
126 | theme: "radical",
127 | });
128 |
129 | const styleTag = document.querySelector("style");
130 | const stylesObject = cssToObject(styleTag.innerHTML);
131 |
132 | const headerClassStyles = stylesObject[".header"];
133 | const statClassStyles = stylesObject[".stat"];
134 | const iconClassStyles = stylesObject[".icon"];
135 |
136 | expect(headerClassStyles.fill).toBe("#5a0");
137 | expect(statClassStyles.fill).toBe(`#${themes.radical.text_color}`);
138 | expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`);
139 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
140 | "fill",
141 | `#${themes.radical.bg_color}`,
142 | );
143 | });
144 |
145 | it("should render with all the themes", () => {
146 | Object.keys(themes).forEach((name) => {
147 | document.body.innerHTML = renderStatsCard(stats, {
148 | theme: name,
149 | });
150 |
151 | const styleTag = document.querySelector("style");
152 | const stylesObject = cssToObject(styleTag.innerHTML);
153 |
154 | const headerClassStyles = stylesObject[".header"];
155 | const statClassStyles = stylesObject[".stat"];
156 | const iconClassStyles = stylesObject[".icon"];
157 |
158 | expect(headerClassStyles.fill).toBe(`#${themes[name].title_color}`);
159 | expect(statClassStyles.fill).toBe(`#${themes[name].text_color}`);
160 | expect(iconClassStyles.fill).toBe(`#${themes[name].icon_color}`);
161 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
162 | "fill",
163 | `#${themes[name].bg_color}`,
164 | );
165 | });
166 | });
167 |
168 | it("should render custom colors with themes and fallback to default colors if invalid", () => {
169 | document.body.innerHTML = renderStatsCard(stats, {
170 | title_color: "invalid color",
171 | text_color: "invalid color",
172 | theme: "radical",
173 | });
174 |
175 | const styleTag = document.querySelector("style");
176 | const stylesObject = cssToObject(styleTag.innerHTML);
177 |
178 | const headerClassStyles = stylesObject[".header"];
179 | const statClassStyles = stylesObject[".stat"];
180 | const iconClassStyles = stylesObject[".icon"];
181 |
182 | expect(headerClassStyles.fill).toBe(`#${themes.default.title_color}`);
183 | expect(statClassStyles.fill).toBe(`#${themes.default.text_color}`);
184 | expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`);
185 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
186 | "fill",
187 | `#${themes.radical.bg_color}`,
188 | );
189 | });
190 |
191 | it("should render icons correctly", () => {
192 | document.body.innerHTML = renderStatsCard(stats, {
193 | show_icons: true,
194 | });
195 |
196 | expect(queryAllByTestId(document.body, "icon")[0]).toBeDefined();
197 | expect(queryByTestId(document.body, "stars")).toBeDefined();
198 | expect(
199 | queryByTestId(document.body, "stars").previousElementSibling, // the label
200 | ).toHaveAttribute("x", "25");
201 | });
202 |
203 | it("should not have icons if show_icons is false", () => {
204 | document.body.innerHTML = renderStatsCard(stats, { show_icons: false });
205 |
206 | expect(queryAllByTestId(document.body, "icon")[0]).not.toBeDefined();
207 | expect(queryByTestId(document.body, "stars")).toBeDefined();
208 | expect(
209 | queryByTestId(document.body, "stars").previousElementSibling, // the label
210 | ).not.toHaveAttribute("x");
211 | });
212 |
213 | it("should auto resize if hide_rank is true", () => {
214 | document.body.innerHTML = renderStatsCard(stats, {
215 | hide_rank: true,
216 | });
217 |
218 | expect(
219 | document.body.getElementsByTagName("svg")[0].getAttribute("width"),
220 | ).toBe("305.81250000000006");
221 | });
222 |
223 | it("should auto resize if hide_rank is true & custom_title is set", () => {
224 | document.body.innerHTML = renderStatsCard(stats, {
225 | hide_rank: true,
226 | custom_title: "Hello world",
227 | });
228 |
229 | expect(
230 | document.body.getElementsByTagName("svg")[0].getAttribute("width"),
231 | ).toBe("270");
232 | });
233 |
234 | it("should render translations", () => {
235 | document.body.innerHTML = renderStatsCard(stats, { locale: "cn" });
236 | expect(document.getElementsByClassName("header")[0].textContent).toBe(
237 | "Anurag Hazra 的 GitHub 统计数据",
238 | );
239 | expect(
240 | document.querySelector(
241 | 'g[transform="translate(0, 0)"]>.stagger>.stat.bold',
242 | ).textContent,
243 | ).toMatchInlineSnapshot(`"获标星数(star):"`);
244 | expect(
245 | document.querySelector(
246 | 'g[transform="translate(0, 25)"]>.stagger>.stat.bold',
247 | ).textContent,
248 | ).toMatchInlineSnapshot(`"累计提交数(commit) (2021):"`);
249 | expect(
250 | document.querySelector(
251 | 'g[transform="translate(0, 50)"]>.stagger>.stat.bold',
252 | ).textContent,
253 | ).toMatchInlineSnapshot(`"拉取请求数(PR):"`);
254 | expect(
255 | document.querySelector(
256 | 'g[transform="translate(0, 75)"]>.stagger>.stat.bold',
257 | ).textContent,
258 | ).toMatchInlineSnapshot(`"指出问题数(issue):"`);
259 | expect(
260 | document.querySelector(
261 | 'g[transform="translate(0, 100)"]>.stagger>.stat.bold',
262 | ).textContent,
263 | ).toMatchInlineSnapshot(`"参与项目数:"`);
264 | });
265 |
266 | it("should render without rounding", () => {
267 | document.body.innerHTML = renderStatsCard(stats, { border_radius: "0" });
268 | expect(document.querySelector("rect")).toHaveAttribute("rx", "0");
269 | document.body.innerHTML = renderStatsCard(stats, {});
270 | expect(document.querySelector("rect")).toHaveAttribute("rx", "4.5");
271 | });
272 | });
273 |
--------------------------------------------------------------------------------
/tests/renderTopLanguages.test.js:
--------------------------------------------------------------------------------
1 | require("@testing-library/jest-dom");
2 | const cssToObject = require("css-to-object");
3 | const renderTopLanguages = require("../src/cards/top-languages-card");
4 |
5 | const { queryByTestId, queryAllByTestId } = require("@testing-library/dom");
6 | const themes = require("../themes");
7 |
8 | describe("Test renderTopLanguages", () => {
9 | const langs = {
10 | HTML: {
11 | color: "#0f0",
12 | name: "HTML",
13 | size: 200,
14 | },
15 | javascript: {
16 | color: "#0ff",
17 | name: "javascript",
18 | size: 200,
19 | },
20 | css: {
21 | color: "#ff0",
22 | name: "css",
23 | size: 100,
24 | },
25 | };
26 |
27 | it("should render correctly", () => {
28 | document.body.innerHTML = renderTopLanguages(langs);
29 |
30 | expect(queryByTestId(document.body, "header")).toHaveTextContent(
31 | "Most Used Languages",
32 | );
33 |
34 | expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent(
35 | "HTML",
36 | );
37 | expect(queryAllByTestId(document.body, "lang-name")[1]).toHaveTextContent(
38 | "javascript",
39 | );
40 | expect(queryAllByTestId(document.body, "lang-name")[2]).toHaveTextContent(
41 | "css",
42 | );
43 | expect(queryAllByTestId(document.body, "lang-progress")[0]).toHaveAttribute(
44 | "width",
45 | "40%",
46 | );
47 | expect(queryAllByTestId(document.body, "lang-progress")[1]).toHaveAttribute(
48 | "width",
49 | "40%",
50 | );
51 | expect(queryAllByTestId(document.body, "lang-progress")[2]).toHaveAttribute(
52 | "width",
53 | "20%",
54 | );
55 | });
56 |
57 | it("should hide languages when hide is passed", () => {
58 | document.body.innerHTML = renderTopLanguages(langs, {
59 | hide: ["HTML"],
60 | });
61 | expect(queryAllByTestId(document.body, "lang-name")[0]).toBeInTheDocument(
62 | "javascript",
63 | );
64 | expect(queryAllByTestId(document.body, "lang-name")[1]).toBeInTheDocument(
65 | "css",
66 | );
67 | expect(queryAllByTestId(document.body, "lang-name")[2]).not.toBeDefined();
68 |
69 | // multiple languages passed
70 | document.body.innerHTML = renderTopLanguages(langs, {
71 | hide: ["HTML", "css"],
72 | });
73 | expect(queryAllByTestId(document.body, "lang-name")[0]).toBeInTheDocument(
74 | "javascript",
75 | );
76 | expect(queryAllByTestId(document.body, "lang-name")[1]).not.toBeDefined();
77 | });
78 |
79 | it("should resize the height correctly depending on langs", () => {
80 | document.body.innerHTML = renderTopLanguages(langs, {});
81 | expect(document.querySelector("svg")).toHaveAttribute("height", "205");
82 |
83 | document.body.innerHTML = renderTopLanguages(
84 | {
85 | ...langs,
86 | python: {
87 | color: "#ff0",
88 | name: "python",
89 | size: 100,
90 | },
91 | },
92 | {},
93 | );
94 | expect(document.querySelector("svg")).toHaveAttribute("height", "245");
95 | });
96 |
97 | it("should render with custom width set", () => {
98 | document.body.innerHTML = renderTopLanguages(langs, {});
99 |
100 | expect(document.querySelector("svg")).toHaveAttribute("width", "300");
101 |
102 | document.body.innerHTML = renderTopLanguages(langs, { card_width: 400 });
103 | expect(document.querySelector("svg")).toHaveAttribute("width", "400");
104 | });
105 |
106 | it("should render default colors properly", () => {
107 | document.body.innerHTML = renderTopLanguages(langs);
108 |
109 | const styleTag = document.querySelector("style");
110 | const stylesObject = cssToObject(styleTag.textContent);
111 |
112 | const headerStyles = stylesObject[".header"];
113 | const langNameStyles = stylesObject[".lang-name"];
114 |
115 | expect(headerStyles.fill).toBe("#2f80ed");
116 | expect(langNameStyles.fill).toBe("#333");
117 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
118 | "fill",
119 | "#fffefe",
120 | );
121 | });
122 |
123 | it("should render custom colors properly", () => {
124 | const customColors = {
125 | title_color: "5a0",
126 | icon_color: "1b998b",
127 | text_color: "9991",
128 | bg_color: "252525",
129 | };
130 |
131 | document.body.innerHTML = renderTopLanguages(langs, { ...customColors });
132 |
133 | const styleTag = document.querySelector("style");
134 | const stylesObject = cssToObject(styleTag.innerHTML);
135 |
136 | const headerStyles = stylesObject[".header"];
137 | const langNameStyles = stylesObject[".lang-name"];
138 |
139 | expect(headerStyles.fill).toBe(`#${customColors.title_color}`);
140 | expect(langNameStyles.fill).toBe(`#${customColors.text_color}`);
141 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
142 | "fill",
143 | "#252525",
144 | );
145 | });
146 |
147 | it("should render custom colors with themes", () => {
148 | document.body.innerHTML = renderTopLanguages(langs, {
149 | title_color: "5a0",
150 | theme: "radical",
151 | });
152 |
153 | const styleTag = document.querySelector("style");
154 | const stylesObject = cssToObject(styleTag.innerHTML);
155 |
156 | const headerStyles = stylesObject[".header"];
157 | const langNameStyles = stylesObject[".lang-name"];
158 |
159 | expect(headerStyles.fill).toBe("#5a0");
160 | expect(langNameStyles.fill).toBe(`#${themes.radical.text_color}`);
161 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
162 | "fill",
163 | `#${themes.radical.bg_color}`,
164 | );
165 | });
166 |
167 | it("should render with all the themes", () => {
168 | Object.keys(themes).forEach((name) => {
169 | document.body.innerHTML = renderTopLanguages(langs, {
170 | theme: name,
171 | });
172 |
173 | const styleTag = document.querySelector("style");
174 | const stylesObject = cssToObject(styleTag.innerHTML);
175 |
176 | const headerStyles = stylesObject[".header"];
177 | const langNameStyles = stylesObject[".lang-name"];
178 |
179 | expect(headerStyles.fill).toBe(`#${themes[name].title_color}`);
180 | expect(langNameStyles.fill).toBe(`#${themes[name].text_color}`);
181 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
182 | "fill",
183 | `#${themes[name].bg_color}`,
184 | );
185 | });
186 | });
187 |
188 | it("should render with layout compact", () => {
189 | document.body.innerHTML = renderTopLanguages(langs, { layout: "compact" });
190 |
191 | expect(queryByTestId(document.body, "header")).toHaveTextContent(
192 | "Most Used Languages",
193 | );
194 |
195 | expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent(
196 | "HTML 40.00%",
197 | );
198 | expect(queryAllByTestId(document.body, "lang-progress")[0]).toHaveAttribute(
199 | "width",
200 | "120.00",
201 | );
202 |
203 | expect(queryAllByTestId(document.body, "lang-name")[1]).toHaveTextContent(
204 | "javascript 40.00%",
205 | );
206 | expect(queryAllByTestId(document.body, "lang-progress")[1]).toHaveAttribute(
207 | "width",
208 | "120.00",
209 | );
210 |
211 | expect(queryAllByTestId(document.body, "lang-name")[2]).toHaveTextContent(
212 | "css 20.00%",
213 | );
214 | expect(queryAllByTestId(document.body, "lang-progress")[2]).toHaveAttribute(
215 | "width",
216 | "60.00",
217 | );
218 | });
219 |
220 | it("should render a translated title", () => {
221 | document.body.innerHTML = renderTopLanguages(langs, { locale: "cn" });
222 | expect(document.getElementsByClassName("header")[0].textContent).toBe(
223 | "最常用的语言",
224 | );
225 | });
226 |
227 | it("should render without rounding", () => {
228 | document.body.innerHTML = renderTopLanguages(langs, { border_radius: "0" });
229 | expect(document.querySelector("rect")).toHaveAttribute("rx", "0");
230 | document.body.innerHTML = renderTopLanguages(langs, { });
231 | expect(document.querySelector("rect")).toHaveAttribute("rx", "4.5");
232 | });
233 | });
234 |
--------------------------------------------------------------------------------
/tests/renderWakatimeCard.test.js:
--------------------------------------------------------------------------------
1 | require("@testing-library/jest-dom");
2 | const renderWakatimeCard = require("../src/cards/wakatime-card");
3 |
4 | const { wakaTimeData } = require("./fetchWakatime.test");
5 |
6 | describe("Test Render Wakatime Card", () => {
7 | it("should render correctly", () => {
8 | const card = renderWakatimeCard(wakaTimeData.data);
9 |
10 | expect(card).toMatchSnapshot();
11 | });
12 |
13 | it("should render correctly with compact layout", () => {
14 | const card = renderWakatimeCard(wakaTimeData.data, { layout: "compact" });
15 |
16 | expect(card).toMatchSnapshot();
17 | });
18 |
19 | it("should render translations", () => {
20 | document.body.innerHTML = renderWakatimeCard({}, { locale: "cn" });
21 | expect(document.getElementsByClassName("header")[0].textContent).toBe(
22 | "Wakatime 周统计",
23 | );
24 | expect(
25 | document.querySelector('g[transform="translate(0, 0)"]>text.stat.bold')
26 | .textContent,
27 | ).toBe("本周没有编程活动");
28 | });
29 |
30 | it("should render without rounding", () => {
31 | document.body.innerHTML = renderWakatimeCard(wakaTimeData.data, { border_radius: "0" });
32 | expect(document.querySelector("rect")).toHaveAttribute("rx", "0");
33 | document.body.innerHTML = renderWakatimeCard(wakaTimeData.data, { });
34 | expect(document.querySelector("rect")).toHaveAttribute("rx", "4.5");
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/tests/retryer.test.js:
--------------------------------------------------------------------------------
1 | require("@testing-library/jest-dom");
2 | const retryer = require("../src/common/retryer");
3 | const { logger } = require("../src/common/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/cards/top-languages-card");
6 | const { renderError } = require("../src/common/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/common/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/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 | "vue-dark": {
93 | title_color: "41b883",
94 | icon_color: "41b883",
95 | text_color: "fffefe",
96 | bg_color: "273849",
97 | },
98 | "shades-of-purple": {
99 | title_color: "fad000",
100 | icon_color: "b362ff",
101 | text_color: "a599e9",
102 | bg_color: "2d2b55",
103 | },
104 | nightowl: {
105 | title_color: "c792ea",
106 | icon_color: "ffeb95",
107 | text_color: "7fdbca",
108 | bg_color: "011627",
109 | },
110 | buefy: {
111 | title_color: "7957d5",
112 | icon_color: "ff3860",
113 | text_color: "363636",
114 | bg_color: "ffffff",
115 | },
116 | "blue-green": {
117 | title_color: "2f97c1",
118 | icon_color: "f5b700",
119 | text_color: "0cf574",
120 | bg_color: "040f0f",
121 | },
122 | algolia: {
123 | title_color: "00AEFF",
124 | icon_color: "2DDE98",
125 | text_color: "FFFFFF",
126 | bg_color: "050F2C",
127 | },
128 | "great-gatsby": {
129 | title_color: "ffa726",
130 | icon_color: "ffb74d",
131 | text_color: "ffd95b",
132 | bg_color: "000000",
133 | },
134 | darcula: {
135 | title_color: "BA5F17",
136 | icon_color: "84628F",
137 | text_color: "BEBEBE",
138 | bg_color: "242424",
139 | },
140 | bear: {
141 | title_color: "e03c8a",
142 | icon_color: "00AEFF",
143 | text_color: "bcb28d",
144 | bg_color: "1f2023",
145 | },
146 | "solarized-dark": {
147 | title_color: "268bd2",
148 | icon_color: "b58900",
149 | text_color: "859900",
150 | bg_color: "002b36",
151 | },
152 | "solarized-light": {
153 | title_color: "268bd2",
154 | icon_color: "b58900",
155 | text_color: "859900",
156 | bg_color: "fdf6e3",
157 | },
158 | "chartreuse-dark": {
159 | title_color: "7fff00",
160 | icon_color: "00AEFF",
161 | text_color: "fff",
162 | bg_color: "000",
163 | },
164 | nord: {
165 | title_color: "81a1c1",
166 | text_color: "d8dee9",
167 | icon_color: "88c0d0",
168 | bg_color: "2e3440",
169 | },
170 | gotham: {
171 | title_color: "2aa889",
172 | icon_color: "599cab",
173 | text_color: "99d1ce",
174 | bg_color: "0c1014",
175 | },
176 | "material-palenight": {
177 | title_color: "c792ea",
178 | icon_color: "89ddff",
179 | text_color: "a6accd",
180 | bg_color: "292d3e",
181 | },
182 | graywhite: {
183 | title_color: "24292e",
184 | icon_color: "24292e",
185 | text_color: "24292e",
186 | bg_color: "ffffff",
187 | },
188 | "vision-friendly-dark": {
189 | title_color: "ffb000",
190 | icon_color: "785ef0",
191 | text_color: "ffffff",
192 | bg_color: "000000",
193 | },
194 | "ayu-mirage": {
195 | title_color: "f4cd7c",
196 | icon_color: "73d0ff",
197 | text_color: "c7c8c2",
198 | bg_color: "1f2430",
199 | },
200 | "midnight-purple": {
201 | title_color: "9745f5",
202 | icon_color: "9f4bff",
203 | text_color: "ffffff",
204 | bg_color: "000000",
205 | },
206 | calm: {
207 | title_color: "e07a5f",
208 | icon_color: "edae49",
209 | text_color: "ebcfb2",
210 | bg_color: "373f51",
211 | },
212 | "flag-india": {
213 | title_color: "ff8f1c",
214 | icon_color: "250E62",
215 | text_color: "509E2F",
216 | bg_color: "ffffff",
217 | },
218 | omni: {
219 | title_color: "FF79C6",
220 | icon_color: "e7de79",
221 | text_color: "E1E1E6",
222 | bg_color: "191622",
223 | },
224 | react: {
225 | title_color: "61dafb",
226 | icon_color: "61dafb",
227 | text_color: "ffffff",
228 | bg_color: "20232a",
229 | },
230 | jolly: {
231 | title_color: "ff64da",
232 | icon_color: "a960ff",
233 | text_color: "ffffff",
234 | bg_color: "291B3E",
235 | },
236 | maroongold: {
237 | title_color: "F7EF8A",
238 | icon_color: "F7EF8A",
239 | text_color: "E0AA3E",
240 | bg_color: "260000",
241 | },
242 | yeblu: {
243 | title_color: "ffff00",
244 | icon_color: "ffff00",
245 | text_color: "ffffff",
246 | bg_color: "002046",
247 | },
248 | blueberry: {
249 | title_color: "82aaff",
250 | icon_color: "89ddff",
251 | text_color: "27e8a7",
252 | bg_color: "242938",
253 | },
254 | slateorange: {
255 | title_color: "faa627",
256 | icon_color: "faa627",
257 | text_color: "ffffff",
258 | bg_color: "36393f",
259 | },
260 | kacho_ga: {
261 | title_color: "bf4a3f",
262 | icon_color: "a64833",
263 | text_color: "d9c8a9",
264 | bg_color: "402b23",
265 | },
266 | outrun:{
267 | title_color: "ffcc00",
268 | icon_color: "ff1aff",
269 | text_color: "8080ff",
270 | bg_color: "141439",
271 | },
272 | ocean_dark:{
273 | title_color: "8957B2",
274 | icon_color: "FFFFFF",
275 | text_color: "92D534",
276 | bg_color: "151A28",
277 | }
278 | };
279 |
280 | module.exports = themes;
281 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "redirects": [
3 | {
4 | "source": "/",
5 | "destination": "https://github.com/anuraghazra/github-readme-stats"
6 | }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------