├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── stale.yml └── workflows │ ├── publish-wiki.yml │ └── test-and-lint.yml ├── .gitignore ├── .node-version ├── .prettierignore ├── .prettierrc.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── action.yml ├── api ├── cards │ ├── most-commit-language.ts │ ├── productive-time.ts │ ├── profile-details.ts │ ├── repos-per-language.ts │ └── stats.ts ├── pages │ └── demo.html ├── theme.ts └── utils │ ├── error-card.ts │ └── github-token-updater.ts ├── dist ├── index.js ├── index.js.map ├── licenses.txt ├── sourcemap-register.js ├── xhr-sync-worker.js └── xhr-sync-worker1.js ├── docs └── README.zh-tw.md ├── jest.config.json ├── package-lock.json ├── package.json ├── src ├── app.ts ├── cards │ ├── most-commit-language-card.ts │ ├── productive-time-card.ts │ ├── profile-details-card.ts │ ├── repos-per-language-card.ts │ └── stats-card.ts ├── const │ ├── icon.ts │ └── theme.ts ├── github-api │ ├── commits-per-language.ts │ ├── contributions-by-year.ts │ ├── productive-time.ts │ ├── profile-details.ts │ └── repos-per-language.ts ├── templates │ ├── card.ts │ ├── donut-chart-card.ts │ ├── productive-time-card.ts │ ├── profile-details-card.ts │ └── stats-card.ts └── utils │ ├── file-writer.ts │ ├── request.ts │ └── translator.ts ├── tests ├── const │ └── theme.test.ts ├── github-api │ ├── commits-per-language.test.ts │ ├── contributions-by-year.test.ts │ ├── profile-details.test.ts │ └── repos-per-language.test.ts └── utils │ └── file-writer.test.ts ├── tsconfig.json ├── vercel.json └── wiki ├── Home.md ├── Tutorial.md ├── Tutorial_legacy.md ├── Videos.md ├── assets_legacy ├── commit-file.png ├── commit-secret.png ├── copy-token-value.png ├── create-token.png ├── edit-workflow-file.png ├── find-developer-settings.png ├── find-personal-access-tokens.png ├── find-repo-settings.png ├── find-secrets.png ├── find-setting.png ├── find-workflow-file.png ├── finish.png ├── generate-new-token.png ├── generate-token-button.png ├── new-file.png ├── new-repo-secret-button.png ├── new-repo.png ├── new-secrect.png ├── old-secret.png ├── output.png ├── permission-1.png ├── permission-2.png ├── press-use-template.png ├── run-workflow.png ├── secret-preview.png ├── type-file-name.png ├── type-repo-name.png ├── type-token-and-token-value.png ├── where-is-action.png └── where-is-add-file.png └── assets_new ├── Video_Full_Res ├── Step1.mp4 ├── Step2_Sub1.mp4 ├── Step3.mp4 ├── Step4.mp4 ├── Step6.mp4 ├── Step7.mp4 └── step5.mp4 ├── create_n_wrkflw.gif ├── edit_rdm.gif ├── edit_wrkflw.gif ├── gen_user_pac.gif ├── make_n_scrt.gif ├── make_rep_f_tmp.gif ├── run_wrkflw.gif └── special_repo.png /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true, 5 | "jest": true 6 | }, 7 | "plugins": ["jest", "prettier"], 8 | "extends": ["google", "plugin:prettier/recommended"], 9 | "parser": "@typescript-eslint/parser", 10 | "parserOptions": { 11 | "ecmaVersion": 12, 12 | "sourceType": "module", 13 | "project": "./tsconfig.json" 14 | }, 15 | "rules": { 16 | "prettier/prettier": "error", 17 | "require-jsdoc": 0 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/** -diff linguist-generated=true 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature request]" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | --- 10 | 11 | name: 🚀 Feature request 12 | about: If you have a feature request 💡 13 | 14 | --- 15 | 16 | **Context** 17 | 18 | What are you trying to do and how would you want to do it differently? Is it something you currently you cannot do? Is this related to an issue/problem? 19 | 20 | **Alternatives** 21 | 22 | Can you achieve the same result doing it in an alternative way? Is the alternative considerable? 23 | 24 | **Has the feature been requested before?** 25 | 26 | Please provide a link to the issue. 27 | 28 | **If the feature request is approved, would you be willing to submit a PR?** 29 | 30 | Yes / No _(Help can be provided if you need assistance submitting a PR)_ 31 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 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 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/publish-wiki.yml: -------------------------------------------------------------------------------- 1 | name: Publish wiki 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: 7 | - wiki/** 8 | - .github/workflows/publish-wiki.yml 9 | 10 | concurrency: 11 | group: publish-wiki 12 | cancel-in-progress: true 13 | 14 | permissions: 15 | contents: write 16 | 17 | jobs: 18 | publish-wiki: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: Andrew-Chen-Wang/github-wiki-action@v4 23 | with: 24 | token: ${{ secrets.REPO_WIKI_WRITE_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/workflows/test-and-lint.yml: -------------------------------------------------------------------------------- 1 | name: Test and Lint 2 | 3 | on: 4 | push: 5 | branches-ignore: [ release ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Collect Workflow Telemetry 14 | uses: catchpoint/foresight-workflow-kit-action@v1 15 | if: success() || failure() 16 | with: 17 | api_key: ${{ secrets.foresight_api_key }} 18 | 19 | - uses: actions/checkout@v4 20 | - name: Use Node.js 20 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version-file: '.node-version' 24 | cache: 'npm' 25 | - run: npm install 26 | - run: npm run test 27 | 28 | - name: Analyze Test and/or Coverage Results 29 | uses: catchpoint/foresight-test-kit-action@v1 30 | if: success() || failure() 31 | with: 32 | api_key: ${{ secrets.foresight_api_key }} 33 | test_format: JUNIT 34 | test_framework: JEST 35 | test_path: ./junit.xml 36 | coverage_format: JACOCO/XML 37 | coverage_path: ./coverage 38 | 39 | lint: 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v4 43 | - name: Use Node.js 20 44 | uses: actions/setup-node@v4 45 | with: 46 | node-version-file: '.node-version' 47 | cache: 'npm' 48 | - run: npm install 49 | - run: npm run lint 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Gatsby files 84 | .cache/ 85 | # Comment in the public line in if your project uses Gatsby and not Next.js 86 | # https://nextjs.org/blog/next-9-1#public-directory-support 87 | # public 88 | 89 | # vuepress build output 90 | .vuepress/dist 91 | 92 | # Serverless directories 93 | .serverless/ 94 | 95 | # FuseBox cache 96 | .fusebox/ 97 | 98 | # DynamoDB Local files 99 | .dynamodb/ 100 | 101 | # TernJS port file 102 | .tern-port 103 | 104 | # Stores VSCode versions used for testing VSCode extensions 105 | .vscode-test 106 | 107 | # yarn v2 108 | .yarn/cache 109 | .yarn/unplugged 110 | .yarn/build-state.yml 111 | .yarn/install-state.gz 112 | .pnp.* 113 | 114 | # Ignore output 115 | profile-summary-card-output/ 116 | 117 | # ctag 118 | tags 119 | tags.* 120 | 121 | # vercel files 122 | .vercel 123 | 124 | # Ignore built ts files 125 | lib/**/* 126 | tests/runner/* 127 | 128 | # Ignore jest report 129 | junit.xml 130 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20.12.2 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | coverage/ 3 | dist/ 4 | list/ 5 | node_modules/ 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": false, 9 | "arrowParens": "avoid" 10 | } 11 | -------------------------------------------------------------------------------- /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 vn7n24fzkq@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 | 2 | # Contributing to github-profile-summary-cards 3 | 4 | First off, thanks for taking the time to contribute! ❤️ 5 | 6 | All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉 7 | 8 | > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: 9 | > - Star the project 10 | > - Tweet about it 11 | > - Refer this project in your project's readme 12 | > - Mention the project at local meetups and tell your friends/colleagues 13 | 14 | 15 | ## Table of Contents 16 | 17 | - [Code of Conduct](#code-of-conduct) 18 | - [I Have a Question](#i-have-a-question) 19 | - [I Want To Contribute](#i-want-to-contribute) 20 | - [Reporting Bugs](#reporting-bugs) 21 | - [Before Submitting a Bug Report](#before-submitting-a-bug-report) 22 | - [How Do I Submit a Good Bug Report?](#how-do-i-submit-a-good-bug-report) 23 | - [Suggesting Enhancements](#suggesting-enhancements) 24 | - [Before Submitting an Enhancement](#before-submitting-an-enhancement) 25 | - [How Do I Submit a Good Enhancement Suggestion?](#how-do-i-submit-a-good-enhancement-suggestion) 26 | - [Your First Code Contribution](#your-first-code-contribution) 27 | - [Improving The Documentation](#improving-the-documentation) 28 | - [Styleguides](#styleguides) 29 | - [Commit Messages](#commit-messages) 30 | - [Join The Project Team](#join-the-project-team) 31 | 32 | ## Code of Conduct 33 | 34 | This project and everyone participating in it is governed by the 35 | [github-profile-summary-cards Code of Conduct](https://github.com/vn7n24fzkq/github-profile-summary-cards/blob/main/CODE_OF_CONDUCT.md). 36 | By participating, you are expected to uphold this code. Please report unacceptable behavior 37 | to the maintainers via [email](mailto:vn7n24fzkq@gmail.com). 38 | 39 | ## I Have a Question 40 | 41 | > If you want to ask a question, we assume that you have read the available [documentation](https://github.com/vn7n24fzkq/github-profile-summary-cards/wiki). 42 | 43 | Before you ask a question, it is best to search for existing [issues](https://github.com/vn7n24fzkq/github-profile-summary-cards/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first. 44 | 45 | If you then still feel the need to ask a question and need clarification, we recommend the following: 46 | 47 | - Open an [issue](https://github.com/vn7n24fzkq/github-profile-summary-cards/issues/new). 48 | - Provide as much context as you can about what you're running into. 49 | - Provide project and platform versions (e.g., Node.js, npm, etc.), depending on what seems relevant. 50 | 51 | We will then take care of the issue as soon as possible. 52 | 53 | ## I Want To Contribute 54 | 55 | > ### Legal Notice 56 | > When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content, and that the content you contribute may be provided under the project license. 57 | 58 | ### Reporting Bugs 59 | 60 | 61 | #### Before Submitting a Bug Report 62 | 63 | A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information, and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. 64 | 65 | - Make sure that you are using the latest version. 66 | - Determine if your bug is really a bug and not an error on your side (e.g., using incompatible environment components/versions). Make sure that you have read the [documentation](https://github.com/vn7n24fzkq/github-profile-summary-cards/wiki). If you are looking for support, you might want to check [this section](#i-have-a-question). 67 | - To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [issue tracker](https://github.com/vn7n24fzkq/github-profile-summary-cards/issues?q=is%3Aissue+label%3Abug). 68 | - Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue. 69 | - Collect information about the bug: 70 | - Stack trace (if applicable) 71 | - OS, Platform, and Version (Windows, Linux, macOS) 72 | - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant. 73 | - Possibly your input and the output 74 | - Can you reliably reproduce the issue? And can you also reproduce it with older versions? 75 | 76 | 77 | #### How Do I Submit a Good Bug Report? 78 | 79 | > **Note:** Please do not report security vulnerabilities or issues containing sensitive information via GitHub issues. Instead, report them to the maintainers via [email](mailto:vn7n24fzkq@gmail.com). 80 | 81 | We use GitHub issues to track bugs and errors. If you run into an issue with the project: 82 | 83 | - Open an [issue](https://github.com/vn7n24fzkq/github-profile-summary-cards/issues/new). 84 | - Explain the behavior you would expect and the actual behavior. 85 | - Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports, you should isolate the problem and create a reduced test case. 86 | - Provide the information you collected in the previous section. 87 | 88 | Once it's filed: 89 | 90 | - The project team will label the issue accordingly. 91 | - A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced. 92 | - If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution). 93 | 94 | ### Suggesting Enhancements 95 | 96 | This section guides you through submitting an enhancement suggestion for github-profile-summary-cards, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions. 97 | 98 | 99 | #### Before Submitting an Enhancement 100 | 101 | - Make sure that you are using the latest version. 102 | - Read the [documentation](https://github.com/vn7n24fzkq/github-profile-summary-cards/wiki) carefully and find out if the functionality is already covered, maybe by an individual configuration. 103 | - Perform a [search](https://github.com/vn7n24fzkq/github-profile-summary-cards/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. 104 | - Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library. 105 | 106 | 107 | #### How Do I Submit a Good Enhancement Suggestion? 108 | 109 | Enhancement suggestions are tracked as [GitHub issues](https://github.com/vn7n24fzkq/github-profile-summary-cards/issues). 110 | 111 | - Use a **clear and descriptive title** for the issue to identify the suggestion. 112 | - Provide a **step-by-step description of the suggested enhancement** in as many details as possible. 113 | - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point, you can also tell which alternatives do not work for you. 114 | - You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. 115 | - **Explain why this enhancement would be useful** to most github-profile-summary-cards users. You may also want to point out other projects that solved it better and which could serve as inspiration. 116 | 117 | ### Your First Code Contribution 118 | 119 | Unsure where to begin contributing to github-profile-summary-cards? You can start by looking through these `good first issue` and `help wanted` issues: 120 | 121 | - [Good First Issues](https://github.com/vn7n24fzkq/github-profile-summary-cards/issues?q=is%3Aissue+is%3Aopen+label%3A"good+first+issue") 122 | - [Help Wanted Issues](https://github.com/vn7n24fzkq/github-profile-summary-cards/issues?q=is%3Aissue+is%3Aopen+label%3A"help+wanted") 123 | 124 | These issues are a great way to get familiar with the codebase. 125 | 126 | To contribute: 127 | 128 | 1. Fork the repository. 129 | 2. Create a new branch: `git checkout -b my-feature-branch` 130 | 3. Make your changes and commit them: `git commit -m 'Add some feature'` 131 | 4. Push to the branch: `git push origin my-feature-branch` 132 | 5. Submit a pull request. 133 | 134 | Please make sure your code follows the project's coding standards and passes all tests. 135 | 136 | ### Improving The Documentation 137 | 138 | Documentation improvements are always welcome! If you find any mistakes or areas that could be improved, feel free to: 139 | 140 | - Open an [issue](https://github.com/vn7n24fzkq/github-profile-summary-cards/issues/new) describing the problem. 141 | - Submit a pull request with your improvements. 142 | 143 | ## Styleguides 144 | 145 | ### Commit Messages 146 | 147 | We follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) style for our commit messages: 148 | 149 | - **feat**: A new feature 150 | - **fix**: A bug fix 151 | - **docs**: Documentation only changes 152 | - **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc.) 153 | - **refactor**: A code change that neither fixes a bug nor adds a feature 154 | - **perf**: A code change that improves performance 155 | - **test**: Adding missing tests or correcting existing tests 156 | - **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation 157 | 158 | Example commit message: 159 | 160 | ``` 161 | feat: add new theme 'sunset' 162 | 163 | Added a new theme called 'sunset' with warm colors. 164 | ``` 165 | 166 | ## Join The Project Team 167 | 168 | If you are interested in becoming a collaborator or maintainer, please reach out! Open an [issue](https://github.com/vn7n24fzkq/github-profile-summary-cards/issues/new) expressing your interest and we can discuss further. 169 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Casper 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

GitHub Profile Summary Cards

3 | 4 | 5 | [繁體中文](./docs/README.zh-tw.md) 6 |

7 | A tool to generate your github summary card for profile README. Inspired by profile-summary-for-github 8 |

9 |

10 | :star: This repo is just for fun, feel free to contribute! :star: 11 |

12 |

13 | 14 | Stargazers 15 | 16 | Releases 17 | 18 | conventionalcommits 19 | 20 | testandlint 21 |

22 |
23 | 24 |
25 |

26 | Get your own cards now!! 27 |

28 | 29 | 30 | ![](https://raw.githubusercontent.com/vn7n24fzkq/vn7n24fzkq/master/profile-summary-card-output/solarized/0-profile-details.svg) 31 | ![](https://raw.githubusercontent.com/vn7n24fzkq/vn7n24fzkq/master/profile-summary-card-output/solarized/1-repos-per-language.svg) 32 | ![](https://raw.githubusercontent.com/vn7n24fzkq/vn7n24fzkq/master/profile-summary-card-output/solarized/2-most-commit-language.svg) 33 | ![](https://raw.githubusercontent.com/vn7n24fzkq/vn7n24fzkq/master/profile-summary-card-output/solarized/3-stats.svg) 34 | ![](https://raw.githubusercontent.com/vn7n24fzkq/vn7n24fzkq/master/profile-summary-card-output/solarized/4-productive-time.svg) 35 | 36 |
37 | 38 | ## Themes 39 | 40 | | | | | | | 41 | |:---:|:---:|:---:|:---:|:---:| 42 | |default|2077|dracula|github|github_dark| 43 | |![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=default)|![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=2077)| ![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=dracula)|![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=github)|![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=github_dark)| 44 | |gruvbox|monokai|nord_bright|nord_dark|radical| 45 | |![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=gruvbox)|![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=monokai)| ![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=nord_bright)|![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=nord_dark) |![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=radical)| 46 | |solarized|solarized_dark|tokyonight|vue|zenburn| 47 | |![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=solarized)|![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=solarized_dark)| ![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=tokyonight)|![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=vue) |![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=zenburn)| 48 | |transparent| 49 | |![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=transparent)| 50 | 51 | [More themes](https://github.com/vn7n24fzkq/github-profile-summary-cards-example/tree/master/profile-summary-card-output) 52 | 53 | ## How to use (API) 54 | ### Profile details card 55 | ![](http://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=nord_bright) 56 | 57 | `http://github-profile-summary-cards.vercel.app/api/cards/profile-details?username={username}&theme={theme_name}` 58 | - Accept url parameters 59 | - theme 60 | - Theme name 61 | - username 62 | - Username 63 | ### Top languages used in repository card 64 | ![](http://github-profile-summary-cards.vercel.app/api/cards/repos-per-language?username=vn7n24fzkq&theme=nord_bright) 65 | 66 | `http://github-profile-summary-cards.vercel.app/api/cards/repos-per-language?username={username}&theme={theme_name}&exclude={exclude}` 67 | - Accept url parameters 68 | - theme 69 | - Theme name 70 | - username 71 | - Username 72 | - exclude: 73 | - A comma separated list of languages to exclude, e.g., exclude=java,rust,jupyter%20Notebook 74 | - You can represent a space in the language list by using '%20' when you want to include a space. 75 | - You can found the supported languages in [here](https://github.com/github/linguist/blob/master/lib/linguist/languages.yml) 76 | 77 | ### Top languages in commits card 78 | ![](http://github-profile-summary-cards.vercel.app/api/cards/most-commit-language?username=vn7n24fzkq&theme=nord_bright) 79 | 80 | `http://github-profile-summary-cards.vercel.app/api/cards/most-commit-language?username={username}&theme={theme_name}&exclude={exclude}` 81 | - Accept url parameters 82 | - theme 83 | - Theme name 84 | - username 85 | - Username 86 | - exclude: 87 | - A comma separated list of languages to exclude, e.g., exclude=java,rust,jupyter%20Notebook 88 | - You can represent a space in the language list by using '%20' when you want to include a space. 89 | - You can found the supported languages in [here](https://github.com/github/linguist/blob/master/lib/linguist/languages.yml) 90 | 91 | ### GitHub stats card 92 | ![](http://github-profile-summary-cards.vercel.app/api/cards/stats?username=vn7n24fzkq&theme=nord_bright&) 93 | 94 | `http://github-profile-summary-cards.vercel.app/api/cards/stats?username={username}&theme={theme_name}` 95 | - Accept url parameters 96 | - theme 97 | - Theme name 98 | - username 99 | - Username 100 | 101 | ### Productive time card 102 | ![](http://github-profile-summary-cards.vercel.app/api/cards/productive-time?username=vn7n24fzkq&theme=nord_bright&utcOffset=8) 103 | 104 | `http://github-profile-summary-cards.vercel.app/api/cards/productive-time?username={username}&theme={theme_name}&utcOffset={utcOffset}` 105 | - accept url parameters 106 | - theme 107 | - username 108 | - utcOffset 109 | 110 | --- 111 | 112 | ## How to use (GitHub Actions) 113 | 114 | This action generate your github profile summary cards and make a commit to your repo. 115 | You can also trigger action by yourself after add this action. 116 | 117 | :star: [Follow tutorial](https://github.com/vn7n24fzkq/github-profile-summary-cards/wiki/Tutorial) ( Recommendation ) :star: 118 | 119 | #### First step 120 | 121 | - You need create a [Personal access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with correct permissions. 122 | [Personal token](https://github.com/vn7n24fzkq/github-profile-summary-cards/wiki/Tutorial#generate-token) 123 | 124 | - Add personal access token to repo secret. 125 | 126 | #### Use template ( create a repository ) 127 | 128 | - [github-profile-summary-cards-example](https://github.com/vn7n24fzkq/github-profile-summary-cards-example) 129 | 130 | - Action already setup in this template, you just need click `use this template button` to create your profile readme. 131 | 132 | - After replace GITHUB_TOKEN with your repo secret and trigger action you can use everything in `profile-summary-card-output` folder. 133 | 134 | #### Add to exist repository 135 | 136 | - Add this action to repo and replace GITHUB_TOKEN in action yml file with your repo secret. 137 | 138 | --- 139 | 140 | ## GitHub Actions usage 141 | 142 | After the action finished. You can see all of summary cards are in folder which named `profile-summary-card-output`. 143 | 144 | `Note: Some summary cards might not be updated in time, because github raw file has cache time.` 145 | 146 | ```yml 147 | name: GitHub-Profile-Summary-Cards 148 | 149 | on: 150 | schedule: # execute every 24 hours 151 | - cron: "* */24 * * *" 152 | workflow_dispatch: 153 | 154 | jobs: 155 | build: 156 | runs-on: ubuntu-latest 157 | name: generate-github-profile-summary-cards 158 | permissions: 159 | contents: write 160 | 161 | steps: 162 | - uses: actions/checkout@v4 163 | - uses: vn7n24fzkq/github-profile-summary-cards@release 164 | env: # default use ${{ secrets.SUMMARY_GITHUB_TOKEN }}, you should replace with your personal access token 165 | GITHUB_TOKEN: ${{ secrets.SUMMARY_GITHUB_TOKEN }} 166 | with: 167 | USERNAME: ${{ github.repository_owner }} 168 | # BRANCH_NAME is optional, default to main, branch name to push cards 169 | BRANCH_NAME: "main" 170 | # UTC_OFFSET is optional, default to zero 171 | UTC_OFFSET: 8 172 | # EXCLUDE is an optional comma seperated list of languages to exclude, defaults to "" 173 | EXCLUDE: "" 174 | # AUTO_PUSH is optional, a boolean variable default to true, whether automatically push generated files to desired branch 175 | AUTO_PUSH: true 176 | ``` 177 | 178 | --- 179 | 180 | ## Local Run 181 | 182 | - Require `node 16`, lower versions should get some problems. 183 | - Add personal access token to `.env` file. ex: `GITHUB_TOKEN=abcda69ddf66ae95538c5b1666591b59b4abc73a` 184 | - Remember `npm run build` after modifying any code 185 | 186 | ```sh 187 | npm run run [username] [UTC offset] 188 | ``` 189 | 190 | Example 191 | 192 | ```sh 193 | npm run run vn7n24fzkq 8 194 | ``` 195 | 196 | - To locally run the API you can use the vercel dev package 197 | 198 | ```sh 199 | vercel dev 200 | ``` 201 | 202 | ## Deploy your own API on Vercel 203 | Quickly deploy your own version! 204 | 205 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvn7n24fzkq%2Fgithub-profile-summary-cards&env=GITHUB_TOKEN&envDescription=https%3A%2F%2Fgithub.com%2Fvn7n24fzkq%2Fgithub-profile-summary-cards%23first-step&project-name=my-github-profile-summary-cards) 206 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: GitHub-Profile-Summary-Cards 2 | description: Generate profile summary cards and commit to default branch 3 | author: vn7n24fzkq 4 | 5 | inputs: 6 | USERNAME: 7 | type: string 8 | required: true 9 | description: 'GitHub username' 10 | default: ${{ github.repository_owner }} 11 | BRANCH_NAME: 12 | type: string 13 | required: false 14 | description: 'The branch to push cards' 15 | default: 'main' 16 | UTC_OFFSET: 17 | type: string 18 | required: false 19 | description: 'The UTC offset used in the Productive Time Card.(e.g., 8, -3)' 20 | default: 0 21 | EXCLUDE: 22 | type: string 23 | required: false 24 | description: 'A comma separated list of languages to exclude' 25 | default: '' 26 | AUTO_PUSH: 27 | type: boolean 28 | required: false 29 | description: 'Whether automatically push generated files to desired branch' 30 | default: true 31 | 32 | runs: 33 | using: 'node20' 34 | main: 'dist/index.js' 35 | 36 | branding: 37 | icon: 'activity' 38 | color: 'orange' 39 | -------------------------------------------------------------------------------- /api/cards/most-commit-language.ts: -------------------------------------------------------------------------------- 1 | import {getCommitsLanguageSVGWithThemeName} from '../../src/cards/most-commit-language-card'; 2 | import {changToNextGitHubToken} from '../utils/github-token-updater'; 3 | import {getErrorMsgCard} from '../utils/error-card'; 4 | import {translateLanguage} from '../../src/utils/translator'; 5 | import type {VercelRequest, VercelResponse} from '@vercel/node'; 6 | 7 | export default async (req: VercelRequest, res: VercelResponse) => { 8 | let {username, theme = 'default', exclude = ''} = req.query; 9 | 10 | if (typeof theme !== 'string') { 11 | res.status(400).send('theme must be a string'); 12 | return; 13 | } 14 | if (typeof username !== 'string') { 15 | res.status(400).send('username must be a string'); 16 | return; 17 | } 18 | if (typeof exclude !== 'string') { 19 | res.status(400).send('exclude must be a string'); 20 | return; 21 | } 22 | let excludeArr = []; 23 | exclude.split(',').forEach(function (val) { 24 | const translatedLanguage = translateLanguage(val); 25 | excludeArr.push(translatedLanguage.toLowerCase()); 26 | }); 27 | 28 | try { 29 | let tokenIndex = 0; 30 | while (true) { 31 | try { 32 | const cardSVG = await getCommitsLanguageSVGWithThemeName(username, theme, excludeArr); 33 | res.setHeader('Content-Type', 'image/svg+xml'); 34 | res.send(cardSVG); 35 | return; 36 | } catch (err: any) { 37 | console.log(err.message); 38 | // We update github token and try again, until getNextGitHubToken throw an Error 39 | changToNextGitHubToken(tokenIndex); 40 | tokenIndex += 1; 41 | } 42 | } 43 | } catch (err: any) { 44 | console.log(err); 45 | res.send(getErrorMsgCard(err.message, theme)); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /api/cards/productive-time.ts: -------------------------------------------------------------------------------- 1 | import {getProductiveTimeSVGWithThemeName} from '../../src/cards/productive-time-card'; 2 | import {changToNextGitHubToken} from '../utils/github-token-updater'; 3 | import {getErrorMsgCard} from '../utils/error-card'; 4 | import type {VercelRequest, VercelResponse} from '@vercel/node'; 5 | 6 | export default async (req: VercelRequest, res: VercelResponse) => { 7 | const {username, theme = 'default', utcOffset = '0'} = req.query; 8 | if (typeof theme !== 'string') { 9 | res.status(400).send('theme must be a string'); 10 | return; 11 | } 12 | if (typeof username !== 'string') { 13 | res.status(400).send('username must be a string'); 14 | return; 15 | } 16 | if (typeof utcOffset !== 'string') { 17 | res.status(400).send('utcOffset must be a string'); 18 | return; 19 | } 20 | try { 21 | let tokenIndex = 0; 22 | while (true) { 23 | try { 24 | const cardSVG = await getProductiveTimeSVGWithThemeName(username, theme, Number(utcOffset)); 25 | res.setHeader('Content-Type', 'image/svg+xml'); 26 | res.send(cardSVG); 27 | return; 28 | } catch (err: any) { 29 | console.log(err.message); 30 | // We update github token and try again, until getNextGitHubToken throw an Error 31 | changToNextGitHubToken(tokenIndex); 32 | tokenIndex += 1; 33 | } 34 | } 35 | } catch (err: any) { 36 | console.log(err); 37 | res.send(getErrorMsgCard(err.message, theme)); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /api/cards/profile-details.ts: -------------------------------------------------------------------------------- 1 | import {getProfileDetailsSVGWithThemeName} from '../../src/cards/profile-details-card'; 2 | import {changToNextGitHubToken} from '../utils/github-token-updater'; 3 | import {getErrorMsgCard} from '../utils/error-card'; 4 | import type {VercelRequest, VercelResponse} from '@vercel/node'; 5 | 6 | export default async (req: VercelRequest, res: VercelResponse) => { 7 | const {username, theme = 'default'} = req.query; 8 | if (typeof theme !== 'string') { 9 | res.status(400).send('theme must be a string'); 10 | return; 11 | } 12 | if (typeof username !== 'string') { 13 | res.status(400).send('username must be a string'); 14 | return; 15 | } 16 | try { 17 | let tokenIndex = 0; 18 | while (true) { 19 | try { 20 | const cardSVG = await getProfileDetailsSVGWithThemeName(username, theme); 21 | res.setHeader('Content-Type', 'image/svg+xml'); 22 | res.send(cardSVG); 23 | return; 24 | } catch (err: any) { 25 | console.log(err.message); 26 | // We update github token and try again, until getNextGitHubToken throw an Error 27 | changToNextGitHubToken(tokenIndex); 28 | tokenIndex += 1; 29 | } 30 | } 31 | } catch (err: any) { 32 | console.log(err); 33 | res.send(getErrorMsgCard(err.message, theme)); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /api/cards/repos-per-language.ts: -------------------------------------------------------------------------------- 1 | import {getReposPerLanguageSVGWithThemeName} from '../../src/cards/repos-per-language-card'; 2 | import {changToNextGitHubToken} from '../utils/github-token-updater'; 3 | import {getErrorMsgCard} from '../utils/error-card'; 4 | import {translateLanguage} from '../../src/utils/translator'; 5 | import type {VercelRequest, VercelResponse} from '@vercel/node'; 6 | 7 | export default async (req: VercelRequest, res: VercelResponse) => { 8 | let {username, theme = 'default', exclude = ''} = req.query; 9 | 10 | if (typeof theme !== 'string') { 11 | res.status(400).send('theme must be a string'); 12 | return; 13 | } 14 | if (typeof username !== 'string') { 15 | res.status(400).send('username must be a string'); 16 | return; 17 | } 18 | if (typeof exclude !== 'string') { 19 | res.status(400).send('exclude must be a string'); 20 | return; 21 | } 22 | let excludeArr = []; 23 | exclude.split(',').forEach(function (val) { 24 | const translatedLanguage = translateLanguage(val); 25 | excludeArr.push(translatedLanguage.toLowerCase()); 26 | }); 27 | 28 | try { 29 | let tokenIndex = 0; 30 | while (true) { 31 | try { 32 | const cardSVG = await getReposPerLanguageSVGWithThemeName(username, theme, excludeArr); 33 | res.setHeader('Content-Type', 'image/svg+xml'); 34 | res.send(cardSVG); 35 | return; 36 | } catch (err: any) { 37 | console.log(err.message); 38 | // We update github token and try again, until getNextGitHubToken throw an Error 39 | changToNextGitHubToken(tokenIndex); 40 | tokenIndex += 1; 41 | } 42 | } 43 | } catch (err: any) { 44 | res.send(getErrorMsgCard(err.message, theme)); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /api/cards/stats.ts: -------------------------------------------------------------------------------- 1 | import {getStatsSVGWithThemeName} from '../../src/cards/stats-card'; 2 | import {changToNextGitHubToken} from '../utils/github-token-updater'; 3 | import {getErrorMsgCard} from '../utils/error-card'; 4 | import type {VercelRequest, VercelResponse} from '@vercel/node'; 5 | 6 | export default async (req: VercelRequest, res: VercelResponse) => { 7 | const {username, theme = 'default'} = req.query; 8 | if (typeof theme !== 'string') { 9 | res.status(400).send('theme must be a string'); 10 | return; 11 | } 12 | if (typeof username !== 'string') { 13 | res.status(400).send('username must be a string'); 14 | return; 15 | } 16 | try { 17 | let tokenIndex = 0; 18 | while (true) { 19 | try { 20 | const cardSVG = await getStatsSVGWithThemeName(username, theme); 21 | res.setHeader('Content-Type', 'image/svg+xml'); 22 | res.send(cardSVG); 23 | return; 24 | } catch (err: any) { 25 | console.log(err.message); 26 | // We update github token and try again, until getNextGitHubToken throw an Error 27 | changToNextGitHubToken(tokenIndex); 28 | tokenIndex += 1; 29 | } 30 | } 31 | } catch (err: any) { 32 | console.log(err); 33 | res.send(getErrorMsgCard(err.message, theme)); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /api/pages/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GitHub Profile Summary Cards 5 | 9 | 13 | 17 | 22 | 23 | 24 | 51 |
52 | 53 | 54 | 55 | 60 | 89 | 90 | 91 | 92 | 93 |
96 | GitHub Profile Summary Cards 97 |
98 |
99 |
100 | 101 | 102 | 103 | 108 | Star 116 | 117 | 118 | 119 | 123 | 129 | 130 | 131 | 132 | 133 | 134 | 140 | 141 | 142 | 150 | 151 | 152 | 153 | 154 | 159 | Submit 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 |
Markdown Usage
169 | 170 | ![]({{ profileDetailSource }}) 171 | 172 |
173 |
174 |
175 |
176 | 177 | 178 | 179 | 180 | 181 |
Markdown Usage
182 | 183 | ![]({{ repoLanguageSource }}) 184 | 185 |
186 |
187 |
188 | 189 | 190 | 191 | 192 |
Markdown Usage
193 | 194 | ![]({{ commitLanguageSource }}) 195 | 196 |
197 |
198 |
199 |
200 | 201 | 202 | 203 | 204 | 205 |
Markdown Usage
206 | ![]({{ statsSource }}) 207 |
208 |
209 |
210 | 211 | 212 | 213 | 214 |
Markdown Usage
215 | 216 | ![]({{ productiveTimeSource }}) 217 | 218 |
219 |
220 |
221 |
222 |
223 | 224 | 225 | GitHub Profile Summary Cards 226 | 227 | 228 |
229 |
230 |
231 | 232 | 233 | 234 | 235 | 277 | 278 | 279 | -------------------------------------------------------------------------------- /api/theme.ts: -------------------------------------------------------------------------------- 1 | import {ThemeMap} from '../src/const/theme'; 2 | import type {VercelRequest, VercelResponse} from '@vercel/node'; 3 | 4 | export default (_: VercelRequest, response: VercelResponse) => { 5 | const themes = Array.from(ThemeMap.keys()); 6 | response.send({themes: themes}); 7 | }; 8 | -------------------------------------------------------------------------------- /api/utils/error-card.ts: -------------------------------------------------------------------------------- 1 | import {Card} from '../../src/templates/card'; 2 | import {Theme, ThemeMap} from '../../src/const/theme'; 3 | 4 | export const getErrorMsgCard = function (msg: string, themeName: string) { 5 | const theme: Theme = ThemeMap.get(themeName)!; 6 | theme.title = 'red'; 7 | 8 | const card = new Card('ERROR!!!', 340, 200, theme); 9 | const svg = card.getSVG(); 10 | const panel = svg.append('g').attr('transform', `translate(30,20)`); 11 | panel.append('text').attr('y', `${card.yPadding}`).style('font-size', `14px`).style('fill', 'red').text(msg); 12 | 13 | return card.toString(); 14 | }; 15 | -------------------------------------------------------------------------------- /api/utils/github-token-updater.ts: -------------------------------------------------------------------------------- 1 | export const changToNextGitHubToken = function (currentIndex: number) { 2 | const tokenName = `GITHUB_TOKEN_${currentIndex + 1}`; 3 | console.log(`Change to ${tokenName}`); 4 | process.env.GITHUB_TOKEN = process.env[tokenName]; 5 | if (!process.env.GITHUB_TOKEN) { 6 | throw new Error('No more GITHUB_TOKEN can be used'); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /dist/xhr-sync-worker.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable no-process-exit */ 3 | const util = require("util"); 4 | const { JSDOM } = require("../../../.."); 5 | const { READY_STATES } = require("./xhr-utils"); 6 | const idlUtils = require("../generated/utils"); 7 | const tough = require("tough-cookie"); 8 | 9 | const dom = new JSDOM(); 10 | const xhr = new dom.window.XMLHttpRequest(); 11 | const xhrImpl = idlUtils.implForWrapper(xhr); 12 | 13 | const chunks = []; 14 | 15 | process.stdin.on("data", chunk => { 16 | chunks.push(chunk); 17 | }); 18 | 19 | process.stdin.on("end", () => { 20 | const buffer = Buffer.concat(chunks); 21 | 22 | const flag = JSON.parse(buffer.toString()); 23 | if (flag.body && flag.body.type === "Buffer" && flag.body.data) { 24 | flag.body = Buffer.from(flag.body.data); 25 | } 26 | if (flag.cookieJar) { 27 | flag.cookieJar = tough.CookieJar.fromJSON(flag.cookieJar); 28 | } 29 | 30 | flag.synchronous = false; 31 | Object.assign(xhrImpl.flag, flag); 32 | const { properties } = xhrImpl; 33 | xhrImpl.readyState = READY_STATES.OPENED; 34 | try { 35 | xhr.addEventListener("loadend", () => { 36 | if (properties.error) { 37 | properties.error = properties.error.stack || util.inspect(properties.error); 38 | } 39 | process.stdout.write(JSON.stringify({ 40 | responseURL: xhrImpl.responseURL, 41 | status: xhrImpl.status, 42 | statusText: xhrImpl.statusText, 43 | properties 44 | }), () => { 45 | process.exit(0); 46 | }); 47 | }, false); 48 | xhr.send(flag.body); 49 | } catch (error) { 50 | properties.error += error.stack || util.inspect(error); 51 | process.stdout.write(JSON.stringify({ 52 | responseURL: xhrImpl.responseURL, 53 | status: xhrImpl.status, 54 | statusText: xhrImpl.statusText, 55 | properties 56 | }), () => { 57 | process.exit(0); 58 | }); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /dist/xhr-sync-worker1.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable no-process-exit */ 3 | const util = require("util"); 4 | const { JSDOM } = require("../../../.."); 5 | const { READY_STATES } = require("./xhr-utils"); 6 | const idlUtils = require("../generated/utils"); 7 | const tough = require("tough-cookie"); 8 | 9 | const dom = new JSDOM(); 10 | const xhr = new dom.window.XMLHttpRequest(); 11 | const xhrImpl = idlUtils.implForWrapper(xhr); 12 | 13 | const chunks = []; 14 | 15 | process.stdin.on("data", chunk => { 16 | chunks.push(chunk); 17 | }); 18 | 19 | process.stdin.on("end", () => { 20 | const buffer = Buffer.concat(chunks); 21 | 22 | const flag = JSON.parse(buffer.toString()); 23 | if (flag.body && flag.body.type === "Buffer" && flag.body.data) { 24 | flag.body = Buffer.from(flag.body.data); 25 | } 26 | if (flag.cookieJar) { 27 | flag.cookieJar = tough.CookieJar.fromJSON(flag.cookieJar); 28 | } 29 | 30 | flag.synchronous = false; 31 | Object.assign(xhrImpl.flag, flag); 32 | const { properties } = xhrImpl; 33 | xhrImpl.readyState = READY_STATES.OPENED; 34 | try { 35 | xhr.addEventListener("loadend", () => { 36 | if (properties.error) { 37 | properties.error = properties.error.stack || util.inspect(properties.error); 38 | } 39 | process.stdout.write(JSON.stringify({ 40 | responseURL: xhrImpl.responseURL, 41 | status: xhrImpl.status, 42 | statusText: xhrImpl.statusText, 43 | properties 44 | }), () => { 45 | process.exit(0); 46 | }); 47 | }, false); 48 | xhr.send(flag.body); 49 | } catch (error) { 50 | properties.error += error.stack || util.inspect(error); 51 | process.stdout.write(JSON.stringify({ 52 | responseURL: xhrImpl.responseURL, 53 | status: xhrImpl.status, 54 | statusText: xhrImpl.statusText, 55 | properties 56 | }), () => { 57 | process.exit(0); 58 | }); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /docs/README.zh-tw.md: -------------------------------------------------------------------------------- 1 | # GitHub Profile Summary Cards 2 | 3 | 這份專案受到 [profile-summary-for-github](https://github.com/tipsy/profile-summary-for-github) 啟發 4 | 5 | ![Test and Lint](https://github.com/vn7n24fzkq/github-profile-summary-cards/workflows/Test%20and%20Lint/badge.svg) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/vn7n24fzkq/github-profile-summary-cards/blob/master/LICENSE) 7 | ![release](https://img.shields.io/github/v/release/vn7n24fzkq/github-profile-summary-cards.svg) 8 | 9 | [繁體中文](./docs/README.zh-tw.md) 10 | 11 | :star: 這份 repo 是好玩才寫的,任何貢獻都很歡迎! :star: 12 | 13 | --- 14 | 15 | ## Markdown 用法 16 | 17 | [馬上試試!!](https://github-profile-summary-cards.vercel.app/demo.html) 18 | 19 | ```![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=vue)``` 20 | 21 | | | | | 22 | |:---:|:---:|:---:| 23 | |default|solarized|monokai| 24 | |![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=default)|![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=solarized)| ![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=monokai)| 25 | |solarized_dark|vue|nord_bright| 26 | |![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=solarized_dark)|![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=vue)| ![](https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=vn7n24fzkq&theme=nord_bright)| 27 | 28 | 29 | ## 範例 30 | 31 | ![](https://raw.githubusercontent.com/vn7n24fzkq/vn7n24fzkq/master/profile-summary-card-output/solarized/0-profile-details.svg) 32 | ![](https://raw.githubusercontent.com/vn7n24fzkq/vn7n24fzkq/master/profile-summary-card-output/solarized/1-repos-per-language.svg) 33 | ![](https://raw.githubusercontent.com/vn7n24fzkq/vn7n24fzkq/master/profile-summary-card-output/solarized/2-most-commit-language.svg) 34 | ![](https://raw.githubusercontent.com/vn7n24fzkq/vn7n24fzkq/master/profile-summary-card-output/solarized/3-stats.svg) 35 | ![](https://raw.githubusercontent.com/vn7n24fzkq/vn7n24fzkq/master/profile-summary-card-output/solarized/4-productive-time.svg) 36 | 37 | [更多主題](https://github.com/vn7n24fzkq/github-profile-summary-cards-example/tree/master/profile-summary-card-output) 38 | 39 | --- 40 | 41 | ## 如何使用 (GitHub Actions) 42 | 43 | 這個 GitHub Action 會產生你的 GitHub 個人統計圖表並且 commit 到你的 repo 裡. 44 | 新增 這個Action 之後你也可以自己觸發 action. 45 | 46 | :star: [跟著教學](https://github.com/vn7n24fzkq/github-profile-summary-cards/wiki/Toturial) ( 推薦 ) :star: 47 | 48 | #### 第一步 49 | 50 | - You need create a [Personal access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with correct permissions. 51 | [Personal token permissions](https://github.com/vn7n24fzkq/github-profile-summary-cards/wiki/Personal-access-token-permissions) 52 | 53 | - Add personal access token to repo secret. 54 | 55 | #### 使用模板 ( 創建一個儲存庫 ) 56 | 57 | - [github-profile-summary-cards-example](https://github.com/vn7n24fzkq/github-profile-summary-cards-example) 58 | 59 | - Action 已經在這個模板裡設定好了, 你只需要按下 `use this template button` 來創建你的 profile readme. 60 | 61 | - 用你儲存庫裡的 secret 來更換 action yml 檔案裡的 GITHUB_TOKEN 並且觸發 action 後你就可以使用所有在 `profile-summary-card-output` 資料夾底下的東西. 62 | 63 | #### 新增到現有的儲存庫 64 | 65 | - 新增這個 action 到儲存庫,並且用你儲存庫裡的 secret 來更換 action yml 檔案裡的 GITHUB_TOKEN. 66 | 67 | --- 68 | 69 | ## GitHub Actions 使用方法 70 | 71 | 在 action 完成之後. 你可以看到所有東西都在名稱為 `profile-summary-card-output` 的資料夾底下. 72 | 73 | `筆記: 所有卡片可能不會立即更新,因為 github raw file 有做 cache` 74 | 75 | ```yml 76 | name: GitHub-Profile-Summary-Cards 77 | 78 | on: 79 | schedule: # execute every 24 hours 80 | - cron: "* */24 * * *" 81 | workflow_dispatch: 82 | 83 | jobs: 84 | build: 85 | runs-on: ubuntu-latest 86 | name: generate-github-profile-summary-cards 87 | 88 | steps: 89 | - uses: actions/checkout@v4 90 | - uses: vn7n24fzkq/github-profile-summary-cards@release 91 | env: # default use ${{ secrets.SUMMARY_GITHUB_TOKEN }}, you should replace with your personal access token 92 | GITHUB_TOKEN: ${{ secrets.SUMMARY_GITHUB_TOKEN }} 93 | with: 94 | USERNAME: ${{ github.repository_owner }} 95 | # BRANCH_NAME is optional, default to main, branch name to push cards 96 | BRANCH_NAME: "main" 97 | # UTC_OFFSET is optional, default to zero 98 | UTC_OFFSET: 8 99 | ``` 100 | 101 | --- 102 | 103 | ## 本地執行 104 | 105 | - 要求 `node 16`, 較低版本可能會出錯。 106 | - 新增 GITHUB_TOKEN 到 `.evn` 檔案裡。 ex:`GITHUB_TOKEN=abcda69ddf66ae95538c5b1666591b59b4abc73a` 107 | - 修改之後記得要 ```npm run build``` 108 | 109 | ``` 110 | npm run run [username] [UTC offset] 111 | ``` 112 | 113 | 範例 114 | ``` 115 | npm run run vn7n24fzkq 8 116 | ``` 117 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectCoverage": true, 3 | "clearMocks": true, 4 | "moduleFileExtensions": ["js", "ts"], 5 | "transform": { 6 | "^.+\\.ts$": "ts-jest" 7 | }, 8 | "setupFilesAfterEnv": [], 9 | "testMatch": ["**/*.test.ts"], 10 | "verbose": true, 11 | "collectCoverageFrom": ["**/*.{ts,jx}", "!**/node_modules/**", "!**/dist/**, !**/lib/**"] 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-profile-summary-cards", 3 | "version": "0.7.0", 4 | "description": "Generate github profile summary cards", 5 | "main": "lib/app.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "format": "prettier --write '**/*.ts'", 9 | "format-check": "prettier --check '**/*.ts'", 10 | "lint": "eslint src/**/*.ts", 11 | "package": "ncc build --source-map --license licenses.txt", 12 | "test": "jest --passWithNoTests --reporters=default --reporters=jest-junit --coverage", 13 | "run": "node -r dotenv/config lib/app.js", 14 | "all": "npm run build && npm run format && npm run lint && npm run package && npm test" 15 | }, 16 | "keywords": [ 17 | "github" 18 | ], 19 | "author": "Casper ", 20 | "license": "MIT", 21 | "dependencies": { 22 | "@actions/core": "^1.9.1", 23 | "@actions/github": "^6.0.0", 24 | "axios": "^1.8.2", 25 | "childprocess": "^2.0.2", 26 | "d3": "^6.7.0", 27 | "dotenv": "^8.2.0", 28 | "jest-junit": "^14.0.0", 29 | "js-abbreviation-number": "^1.4.0", 30 | "jsdom": "^16.4.0", 31 | "moment": "^2.29.4", 32 | "retry-axios": "^2.6.0" 33 | }, 34 | "devDependencies": { 35 | "@types/d3": "^6.7.0", 36 | "@types/jest": "^27.0.2", 37 | "@types/jsdom": "^16.2.13", 38 | "@types/node": "^20.12.7", 39 | "@typescript-eslint/parser": "^5.2.0", 40 | "@vercel/ncc": "^0.38.1", 41 | "@vercel/node": "^1.12.1", 42 | "axios-mock-adapter": "^1.18.2", 43 | "eslint": "^7.22.0", 44 | "eslint-config-google": "^0.14.0", 45 | "eslint-config-prettier": "^8.1.0", 46 | "eslint-plugin-jest": "^25.2.2", 47 | "eslint-plugin-prettier": "^3.3.1", 48 | "jest": "^27.4.7", 49 | "pre-commit": "^1.2.2", 50 | "prettier": "2.2.1", 51 | "ts-jest": "^27.0.7", 52 | "typescript": "^4.4.4" 53 | }, 54 | "pre-commit": [ 55 | "test", 56 | "lint" 57 | ], 58 | "engines": { 59 | "node": "20.x" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import {createProfileDetailsCard} from './cards/profile-details-card'; 3 | import {createReposPerLanguageCard} from './cards/repos-per-language-card'; 4 | import {createCommitsPerLanguageCard} from './cards/most-commit-language-card'; 5 | import {createStatsCard} from './cards/stats-card'; 6 | import {createProductiveTimeCard} from './cards/productive-time-card'; 7 | import {spawn} from 'child_process'; 8 | import {translateLanguage} from './utils/translator'; 9 | import {OUTPUT_PATH, generatePreviewMarkdown} from './utils/file-writer'; 10 | 11 | const execCmd = (cmd: string, args: string[] = []) => 12 | new Promise((resolve, reject) => { 13 | const app = spawn(cmd, args, {stdio: 'pipe'}); 14 | let stdout = ''; 15 | app.stdout.on('data', data => { 16 | stdout = data; 17 | }); 18 | app.on('close', code => { 19 | if (code !== 0 && !stdout.includes('nothing to commit')) { 20 | const err = new Error(`${cmd} ${args} \n ${stdout} \n Invalid status code: ${code}`); 21 | return reject(err); 22 | } 23 | return resolve(code); 24 | }); 25 | app.on('error', reject); 26 | }); 27 | 28 | // ProfileSummaryCardsTemplate 29 | const commitFile = async () => { 30 | await execCmd('git', ['config', '--global', 'user.email', 'profile-summary-cards-bot@example.com']); 31 | await execCmd('git', ['config', '--global', 'user.name', 'profile-summary-cards[bot]']); 32 | await execCmd('git', ['add', OUTPUT_PATH]); 33 | await execCmd('git', ['commit', '-m', 'Generate profile summary cards']); 34 | await execCmd('git', ['push']); 35 | }; 36 | 37 | // main 38 | const action = async () => { 39 | core.info(`Start...`); 40 | const username = core.getInput('USERNAME', {required: true}); 41 | core.info(`Username: ${username}`); 42 | const utcOffset = Number(core.getInput('UTC_OFFSET', {required: false})); 43 | core.info(`UTC offset: ${utcOffset}`); 44 | const exclude = core.getInput('EXCLUDE', {required: false}).split(','); 45 | core.info(`Excluded languages: ${exclude}`); 46 | const autoPush = core.getBooleanInput('AUTO_PUSH', {required: false}); 47 | core.info(`You ${autoPush ? 'have' : "haven't"} set automatically push commits`); 48 | 49 | try { 50 | // Remove old output 51 | core.info(`Remove old cards...`); 52 | await execCmd('sudo', ['rm', '-rf', OUTPUT_PATH]); 53 | 54 | // ProfileDetailsCard 55 | try { 56 | core.info(`Creating ProfileDetailsCard...`); 57 | await createProfileDetailsCard(username); 58 | } catch (error: any) { 59 | core.error(`Error when creating ProfileDetailsCard \n${error.stack}`); 60 | } 61 | 62 | // ReposPerLanguageCard 63 | try { 64 | core.info(`Creating ReposPerLanguageCard...`); 65 | await createReposPerLanguageCard(username, exclude); 66 | } catch (error: any) { 67 | core.error(`Error when creating ReposPerLanguageCard \n${error.stack}`); 68 | } 69 | 70 | // CommitsPerLanguageCard 71 | try { 72 | core.info(`Creating CommitsPerLanguageCard...`); 73 | await createCommitsPerLanguageCard(username, exclude); 74 | } catch (error: any) { 75 | core.error(`Error when creating CommitsPerLanguageCard \n${error.stack}`); 76 | } 77 | 78 | // StatsCard 79 | try { 80 | core.info(`Creating StatsCard...`); 81 | await createStatsCard(username); 82 | } catch (error: any) { 83 | core.error(`Error when creating StatsCard \n${error.stack}`); 84 | } 85 | // ProductiveTimeCard 86 | try { 87 | core.info(`Creating ProductiveTimeCard...`); 88 | await createProductiveTimeCard(username, utcOffset); 89 | } catch (error: any) { 90 | core.error(`Error when creating ProductiveTimeCard \n${error.stack}`); 91 | } 92 | 93 | // generate markdown 94 | try { 95 | core.info(`Creating preview markdown...`); 96 | generatePreviewMarkdown(true); 97 | } catch (error: any) { 98 | core.error(`Error when creating preview markdown \n${error.stack}`); 99 | } 100 | 101 | // Commit changes 102 | if (autoPush) { 103 | core.info(`Commit file...`); 104 | let retry = 0; 105 | const maxRetry = 3; 106 | while (retry < maxRetry) { 107 | retry += 1; 108 | try { 109 | await commitFile(); 110 | } catch (error) { 111 | if (retry == maxRetry) { 112 | throw error; 113 | } 114 | core.warning(`Commit failed. Retry...`); 115 | } 116 | } 117 | } 118 | } catch (error: any) { 119 | core.error(error); 120 | core.setFailed(error.message); 121 | } 122 | }; 123 | 124 | const main = async (username: string, utcOffset: number, exclude: Array) => { 125 | try { 126 | await createProfileDetailsCard(username); 127 | await createReposPerLanguageCard(username, exclude); 128 | await createCommitsPerLanguageCard(username, exclude); 129 | await createStatsCard(username); 130 | await createProductiveTimeCard(username, utcOffset); 131 | generatePreviewMarkdown(false); 132 | } catch (error: any) { 133 | console.error(error); 134 | } 135 | }; 136 | 137 | // program entry point 138 | // check if run in github action 139 | if (process.argv.length == 2) { 140 | action(); 141 | } else { 142 | const username = process.argv[2]; 143 | const utcOffset = Number(process.argv[3]); 144 | const exclude: Array = []; 145 | if (process.argv[4]) { 146 | process.argv[4].split(',').forEach(function (val) { 147 | const translatedLanguage = translateLanguage(val); 148 | exclude.push(translatedLanguage.toLowerCase()); 149 | }); 150 | } 151 | main(username, utcOffset, exclude); 152 | } 153 | -------------------------------------------------------------------------------- /src/cards/most-commit-language-card.ts: -------------------------------------------------------------------------------- 1 | import {ThemeMap} from '../const/theme'; 2 | import {getCommitLanguage, CommitLanguages} from '../github-api/commits-per-language'; 3 | import {createDonutChartCard} from '../templates/donut-chart-card'; 4 | import {writeSVG} from '../utils/file-writer'; 5 | 6 | export const createCommitsPerLanguageCard = async function (username: string, exclude: Array) { 7 | const statsData = await getCommitsLanguageData(username, exclude); 8 | for (const themeName of ThemeMap.keys()) { 9 | const svgString = getCommitsLanguageSVG(statsData, themeName); 10 | // output to folder, use 2- prefix for sort in preview 11 | writeSVG(themeName, '2-most-commit-language', svgString); 12 | } 13 | }; 14 | 15 | export const getCommitsLanguageSVGWithThemeName = async function ( 16 | username: string, 17 | themeName: string, 18 | exclude: Array 19 | ): Promise { 20 | if (!ThemeMap.has(themeName)) throw new Error('Theme does not exist'); 21 | const langData = await getCommitsLanguageData(username, exclude); 22 | return getCommitsLanguageSVG(langData, themeName); 23 | }; 24 | 25 | const getCommitsLanguageSVG = function ( 26 | langData: {name: string; value: number; color: string}[], 27 | themeName: string 28 | ): string { 29 | if (langData.length == 0) { 30 | langData.push({ 31 | name: 'There are no', 32 | value: 1, 33 | color: '#586e75' 34 | }); 35 | langData.push({ 36 | name: 'any commits', 37 | value: 1, 38 | color: '#586e75' 39 | }); 40 | langData.push({ 41 | name: 'in the last year', 42 | value: 1, 43 | color: '#586e75' 44 | }); 45 | } 46 | const svgString = createDonutChartCard('Top Languages by Commit', langData, ThemeMap.get(themeName)!); 47 | return svgString; 48 | }; 49 | 50 | const getCommitsLanguageData = async function ( 51 | username: string, 52 | exclude: Array 53 | ): Promise<{name: string; value: number; color: string}[]> { 54 | const commitLanguages: CommitLanguages = await getCommitLanguage(username, exclude); 55 | let langData = []; 56 | 57 | // make a pie data 58 | for (const [key, value] of commitLanguages.getLanguageMap()) { 59 | langData.push({ 60 | name: key, 61 | value: value.count, 62 | color: value.color 63 | }); 64 | } 65 | langData.sort(function (a, b) { 66 | return b.value - a.value; 67 | }); 68 | langData = langData.slice(0, 5); // get top 5 69 | 70 | return langData; 71 | }; 72 | -------------------------------------------------------------------------------- /src/cards/productive-time-card.ts: -------------------------------------------------------------------------------- 1 | import {ThemeMap} from '../const/theme'; 2 | import {getProductiveTime} from '../github-api/productive-time'; 3 | import {createProductiveCard as productiveTimeCard} from '../templates/productive-time-card'; 4 | import {writeSVG} from '../utils/file-writer'; 5 | 6 | export const createProductiveTimeCard = async function (username: string, utcOffset: number) { 7 | const productiveTimeData = await getProductiveTimeData(username, utcOffset); 8 | for (const themeName of ThemeMap.keys()) { 9 | const svgString = getProductiveTimeSVG(productiveTimeData, themeName, utcOffset); 10 | // output to folder, use 4- prefix for sort in preview 11 | writeSVG(themeName, '4-productive-time', svgString); 12 | } 13 | }; 14 | 15 | export const getProductiveTimeSVGWithThemeName = async function ( 16 | username: string, 17 | themeName: string, 18 | utcOffset: number 19 | ) { 20 | if (!ThemeMap.has(themeName)) throw new Error('Theme does not exist'); 21 | const productiveTimeData = await getProductiveTimeData(username, utcOffset); 22 | return getProductiveTimeSVG(productiveTimeData, themeName, utcOffset); 23 | }; 24 | 25 | const getProductiveTimeSVG = function ( 26 | productiveTimeData: Array, 27 | themeName: string, 28 | utcOffset: number 29 | ): string { 30 | const svgString = productiveTimeCard(productiveTimeData, ThemeMap.get(themeName)!, utcOffset); 31 | return svgString; 32 | }; 33 | 34 | const adjustOffset = function (offset: number, RoundRobin: {offset: number}): number { 35 | if (offset % 1 == 0) { 36 | return offset; 37 | // offset % 1 should be 0.3 or 0.7 but its js and it gives 0.29999 or -0.299999 thats why this frankenstein 38 | } else if ((offset % 1 > 0.29 && offset % 1 < 0.31) || (offset % 1 < -0.29 && offset % 1 > -0.31)) { 39 | // toggle up and down between hour 40 | RoundRobin.offset = (RoundRobin.offset + 1) % 2; 41 | return RoundRobin.offset === 0 ? Math.floor(offset) : Math.ceil(offset); 42 | } else if ((offset % 1 > 0.44 && offset % 1 < 0.46) || (offset % 1 < -0.44 && offset % 1 > -0.45)) { 43 | // distrubute 1 : 3 ratio for 0.45 utc time 44 | RoundRobin.offset = (RoundRobin.offset + 1) % 4; 45 | return RoundRobin.offset === 0 ? Math.floor(offset) : Math.ceil(offset); 46 | } else { 47 | // flood down , if utc is given right it will never be executed 48 | return Math.floor(offset); 49 | } 50 | }; 51 | 52 | const getProductiveTimeData = async function (username: string, utcOffset: number): Promise> { 53 | const until = new Date(); 54 | const since = new Date(); 55 | since.setFullYear(since.getFullYear() - 1); 56 | const productiveTime = await getProductiveTime(username, until.toISOString(), since.toISOString()); 57 | // process productiveTime 58 | const chartData = new Array(24); 59 | chartData.fill(0); 60 | const roundRobin = { 61 | offset: 0 62 | }; 63 | for (const time of productiveTime.productiveDate) { 64 | const hour = new Date(time).getUTCHours(); // We use UTC+0 here 65 | const afterOffset = adjustOffset(Number(hour) + Number(utcOffset), roundRobin); // Add offset to hour 66 | // convert afterOffset to 0-23 67 | if (afterOffset < 0) { 68 | // if afterOffset is negative, we need to add 24 to get the correct hour 69 | chartData[24 + afterOffset] += 1; 70 | } else if (afterOffset > 23) { 71 | // if afterOffset is greater than 23, we need to subtract 24 to get the correct hour 72 | chartData[afterOffset - 24] += 1; 73 | } else { 74 | // if afterOffset is between 0 and 23, we can use it as the hour 75 | chartData[afterOffset] += 1; 76 | } 77 | } 78 | 79 | return chartData; 80 | }; 81 | -------------------------------------------------------------------------------- /src/cards/profile-details-card.ts: -------------------------------------------------------------------------------- 1 | import {ThemeMap} from '../const/theme'; 2 | import {Icon} from '../const/icon'; 3 | import {abbreviateNumber} from 'js-abbreviation-number'; 4 | import {getProfileDetails, ProfileDetails, ProfileContribution} from '../github-api/profile-details'; 5 | import {getContributionByYear} from '../github-api/contributions-by-year'; 6 | import {createDetailCard} from '../templates/profile-details-card'; 7 | import {writeSVG} from '../utils/file-writer'; 8 | 9 | export const createProfileDetailsCard = async function (username: string) { 10 | const profileDetailsData = await getProfileDetailsData(username); 11 | for (const themeName of ThemeMap.keys()) { 12 | const title = 13 | profileDetailsData[0].name == null ? `${username}` : `${username} (${profileDetailsData[0].name})`; 14 | const svgString = getProfileDetailsSVG( 15 | title, 16 | profileDetailsData[0].contributions, 17 | profileDetailsData[1], 18 | themeName 19 | ); 20 | // output to folder, use 0- prefix for sort in preview 21 | writeSVG(themeName, '0-profile-details', svgString); 22 | } 23 | }; 24 | export const getProfileDetailsSVGWithThemeName = async function (username: string, themeName: string): Promise { 25 | if (!ThemeMap.has(themeName)) throw new Error('Theme does not exist'); 26 | const profileDetailsData = await getProfileDetailsData(username); 27 | const title = profileDetailsData[0].name == null ? `${username}` : `${username} (${profileDetailsData[0].name})`; 28 | return getProfileDetailsSVG(title, profileDetailsData[0].contributions, profileDetailsData[1], themeName); 29 | }; 30 | 31 | const getProfileDetailsSVG = function ( 32 | title: string, 33 | contributionsData: ProfileContribution[], 34 | userDetails: {index: number; icon: string; name: string; value: string}[], 35 | themeName: string 36 | ): string { 37 | const svgString = createDetailCard(`${title}`, userDetails, contributionsData, ThemeMap.get(themeName)!); 38 | return svgString; 39 | }; 40 | 41 | const getProfileDateJoined = function (profileDetails: ProfileDetails): string { 42 | const s = (unit: number) => { 43 | return unit === 1 ? '' : 's'; 44 | }; 45 | 46 | const now = Date.now(); 47 | const created = new Date(profileDetails.createdAt); 48 | const diff = new Date(now - created.getTime()); 49 | const years = diff.getUTCFullYear() - new Date(0).getUTCFullYear(); 50 | const months = diff.getUTCMonth() - new Date(0).getUTCMonth(); 51 | const days = diff.getUTCDate() - new Date(0).getUTCDate(); 52 | return years 53 | ? `${years} year${s(years)} ago` 54 | : months 55 | ? `${months} month${s(months)} ago` 56 | : `${days} day${s(days)} ago`; 57 | }; 58 | 59 | const getProfileDetailsData = async function ( 60 | username: string 61 | ): Promise<[ProfileDetails, {index: number; icon: string; name: string; value: string}[]]> { 62 | const profileDetails = await getProfileDetails(username); 63 | let totalContributions = 0; 64 | if (process.env.VERCEL) { 65 | // If running on vercel, we only calculate for last 1 year to avoid hobby timeout limit 66 | profileDetails.contributionYears = profileDetails.contributionYears.slice(0, 1); 67 | for (const year of profileDetails.contributionYears) { 68 | totalContributions += (await getContributionByYear(username, year)).totalContributions; 69 | } 70 | } else { 71 | for (const year of profileDetails.contributionYears) { 72 | totalContributions += (await getContributionByYear(username, year)).totalContributions; 73 | } 74 | } 75 | 76 | const userDetails: {index: number; icon: string; name: string; value: string}[] = [ 77 | // If running on vercel, we only display for last 1 year contributions count 78 | !process.env.VERCEL 79 | ? { 80 | index: 0, 81 | icon: Icon.GITHUB, 82 | name: 'Contributions', 83 | value: `${abbreviateNumber(totalContributions, 2)} Contributions on GitHub` 84 | } 85 | : { 86 | index: 0, 87 | icon: Icon.GITHUB, 88 | name: 'Contributions', 89 | value: `${abbreviateNumber(totalContributions, 2)} Contributions in ${ 90 | profileDetails.contributionYears[0] 91 | }` 92 | }, 93 | { 94 | index: 1, 95 | icon: Icon.REPOS, 96 | name: 'Public Repos', 97 | value: `${abbreviateNumber(profileDetails.totalPublicRepos, 2)} Public Repos` 98 | }, 99 | { 100 | index: 2, 101 | icon: Icon.CLOCK, 102 | name: 'JoinedAt', 103 | value: `Joined GitHub ${getProfileDateJoined(profileDetails)}` 104 | } 105 | ]; 106 | 107 | // hard code here, cuz I'm lazy 108 | if (profileDetails.email) { 109 | userDetails.push({ 110 | index: 3, 111 | icon: Icon.EMAIL, 112 | name: 'Email', 113 | value: profileDetails['email'] 114 | }); 115 | } else if (profileDetails.company) { 116 | userDetails.push({ 117 | index: 3, 118 | icon: Icon.COMPANY, 119 | name: 'Company', 120 | value: profileDetails['company'] 121 | }); 122 | } else if (profileDetails.location) { 123 | userDetails.push({ 124 | index: 3, 125 | icon: Icon.LOCATION, 126 | name: 'Location', 127 | value: profileDetails['location'] 128 | }); 129 | } 130 | 131 | return [profileDetails, userDetails]; 132 | }; 133 | -------------------------------------------------------------------------------- /src/cards/repos-per-language-card.ts: -------------------------------------------------------------------------------- 1 | import {ThemeMap} from '../const/theme'; 2 | import {getRepoLanguages} from '../github-api/repos-per-language'; 3 | import {createDonutChartCard} from '../templates/donut-chart-card'; 4 | import {writeSVG} from '../utils/file-writer'; 5 | 6 | export const createReposPerLanguageCard = async function (username: string, exclude: Array) { 7 | const langData = await getRepoLanguageData(username, exclude); 8 | for (const themeName of ThemeMap.keys()) { 9 | const svgString = getReposPerLanguageSVG(langData, themeName); 10 | // output to folder, use 1- prefix for sort in preview 11 | writeSVG(themeName, '1-repos-per-language', svgString); 12 | } 13 | }; 14 | 15 | export const getReposPerLanguageSVGWithThemeName = async function ( 16 | username: string, 17 | themeName: string, 18 | exclude: Array 19 | ) { 20 | if (!ThemeMap.has(themeName)) throw new Error('Theme does not exist'); 21 | const langData = await getRepoLanguageData(username, exclude); 22 | return getReposPerLanguageSVG(langData, themeName); 23 | }; 24 | 25 | const getReposPerLanguageSVG = function (langData: {name: string; value: number; color: string}[], themeName: string) { 26 | const svgString = createDonutChartCard('Top Languages by Repo', langData, ThemeMap.get(themeName)!); 27 | return svgString; 28 | }; 29 | 30 | const getRepoLanguageData = async function (username: string, exclude: Array) { 31 | const repoLanguages = await getRepoLanguages(username, exclude); 32 | let langData = []; 33 | 34 | // make a pie data 35 | for (const [key, value] of repoLanguages.getLanguageMap()) { 36 | langData.push({ 37 | name: key, 38 | value: value.count, 39 | color: value.color 40 | }); 41 | } 42 | langData.sort(function (a, b) { 43 | return b.value - a.value; 44 | }); 45 | langData = langData.slice(0, 5); // get top 5 46 | return langData; 47 | }; 48 | -------------------------------------------------------------------------------- /src/cards/stats-card.ts: -------------------------------------------------------------------------------- 1 | import {ThemeMap} from '../const/theme'; 2 | import {Icon} from '../const/icon'; 3 | import {abbreviateNumber} from 'js-abbreviation-number'; 4 | import {getProfileDetails} from '../github-api/profile-details'; 5 | import {getContributionByYear} from '../github-api/contributions-by-year'; 6 | import {createStatsCard as statsCard} from '../templates/stats-card'; 7 | import {writeSVG} from '../utils/file-writer'; 8 | 9 | export const createStatsCard = async function (username: string) { 10 | const statsData = await getStatsData(username); 11 | for (const themeName of ThemeMap.keys()) { 12 | const svgString = getStatsSVG(statsData, themeName); 13 | // output to folder, use 3- prefix for sort in preview 14 | writeSVG(themeName, '3-stats', svgString); 15 | } 16 | }; 17 | 18 | export const getStatsSVGWithThemeName = async function (username: string, themeName: string) { 19 | if (!ThemeMap.has(themeName)) throw new Error('Theme does not exist'); 20 | const statsData = await getStatsData(username); 21 | return getStatsSVG(statsData, themeName); 22 | }; 23 | 24 | const getStatsSVG = function ( 25 | StatsData: {index: number; icon: string; name: string; value: string}[], 26 | themeName: string 27 | ) { 28 | const title = 'Stats'; 29 | const svgString = statsCard(`${title}`, StatsData, ThemeMap.get(themeName)!); 30 | return svgString; 31 | }; 32 | 33 | const getStatsData = async function ( 34 | username: string 35 | ): Promise<{index: number; icon: string; name: string; value: string}[]> { 36 | const profileDetails = await getProfileDetails(username); 37 | const totalStars = profileDetails.totalStars; 38 | let totalCommitContributions = 0; 39 | const totalPullRequestContributions = profileDetails.totalPullRequestContributions; 40 | const totalIssueContributions = profileDetails.totalIssueContributions; 41 | 42 | const totalRepositoryContributions = profileDetails.totalRepositoryContributions; 43 | if (process.env.VERCEL) { 44 | // If running on vercel, we only calculate for last 1 year to avoid Vercel timeout limit 45 | profileDetails.contributionYears = profileDetails.contributionYears.slice(0, 1); 46 | for (const year of profileDetails.contributionYears) { 47 | const contributions = await getContributionByYear(username, year); 48 | totalCommitContributions += contributions.totalCommitContributions; 49 | } 50 | } else { 51 | for (const year of profileDetails.contributionYears) { 52 | const contributions = await getContributionByYear(username, year); 53 | totalCommitContributions += contributions.totalCommitContributions; 54 | } 55 | } 56 | 57 | const statsData = [ 58 | { 59 | index: 0, 60 | icon: Icon.STAR, 61 | name: 'Total Stars:', 62 | value: `${abbreviateNumber(totalStars, 1)}` 63 | }, 64 | // If running on vercel, we only display for last 1 year commits count 65 | !process.env.VERCEL 66 | ? { 67 | index: 1, 68 | icon: Icon.COMMIT, 69 | name: 'Total Commits:', 70 | value: `${abbreviateNumber(totalCommitContributions, 1)}` 71 | } 72 | : { 73 | index: 1, 74 | icon: Icon.COMMIT, 75 | name: `${profileDetails.contributionYears[0]} Commits:`, 76 | value: `${abbreviateNumber(totalCommitContributions, 1)}` 77 | }, 78 | { 79 | index: 2, 80 | icon: Icon.PULL_REQUEST, 81 | name: 'Total PRs:', 82 | value: `${abbreviateNumber(totalPullRequestContributions, 1)}` 83 | }, 84 | { 85 | index: 3, 86 | icon: Icon.ISSUE, 87 | name: 'Total Issues:', 88 | value: `${abbreviateNumber(totalIssueContributions, 1)}` 89 | }, 90 | { 91 | index: 4, 92 | icon: Icon.REPOS, 93 | name: 'Contributed to:', 94 | value: `${abbreviateNumber(totalRepositoryContributions, 1)}` 95 | } 96 | ]; 97 | return statsData; 98 | }; 99 | -------------------------------------------------------------------------------- /src/const/icon.ts: -------------------------------------------------------------------------------- 1 | // icons are from https://primer.style/octicons/ 2 | export const Icon = { 3 | GITHUB: ``, 4 | EMAIL: ``, 5 | CLOCK: ``, 6 | REPOS: ``, 7 | COMPANY: ``, 8 | LOCATION: ``, 9 | STAR: ``, 10 | MERGE: ``, 11 | PR: ``, 12 | CODE_REVIEW: ``, 13 | ISSUE: ``, 14 | COMMIT: ``, 15 | PULL_REQUEST: `` 16 | }; 17 | -------------------------------------------------------------------------------- /src/const/theme.ts: -------------------------------------------------------------------------------- 1 | export const ThemeMap = new Map(); 2 | 3 | export class Theme { 4 | title: string; 5 | text: string; 6 | background: string; 7 | stroke: string; 8 | strokeOpacity: number; 9 | icon: string; 10 | chart: string; 11 | constructor( 12 | title: string, 13 | text: string, 14 | background: string, 15 | stroke: string, 16 | strokeOpacity: number, 17 | icon: string, 18 | chart: string 19 | ) { 20 | this.title = title; 21 | this.text = text; 22 | this.background = background; 23 | this.stroke = stroke; 24 | this.strokeOpacity = strokeOpacity; 25 | this.icon = icon; 26 | this.chart = chart; 27 | } 28 | } 29 | 30 | // Set up themes 31 | // We support short hex color, hex color and RGBA hex 32 | // ThemeMap.set(name, new Theme('title', 'text', 'background', 'stroke', 'strokeOpacity', 'icon', 'chart')); 33 | ThemeMap.set('holi', new Theme('#5ea9eb', '#d6e7ff', '#030314', '#d6e7ff', 1, '#5090cb', '#5090cb')); 34 | ThemeMap.set('2077', new Theme('#ff0055', '#03d8f3', '#141321', '#141321', 1, '#fcee0c', '#00ffc8')); 35 | ThemeMap.set('algolia', new Theme('#00aeff', '#ffffff', '#050f2c', '#000000', 0, '#2dde98', '#00aeff')); 36 | ThemeMap.set('apprentice', new Theme('#ffffff', '#bcbcbc', '#262626', '#000000', 0, '#ffffaf', '#ffffff')); 37 | ThemeMap.set('aura_dark', new Theme('#ff7372', '#dbdbdb', '#252334', '#000000', 0, '#6cffd0', '#ff7372')); 38 | ThemeMap.set('aura', new Theme('#a277ff', '#61ffca', '#15141b', '#000000', 0, '#ffca85', '#a277ff')); 39 | ThemeMap.set('ayu_mirage', new Theme('#f4cd7c', '#c7c8c2', '#1f2430', '#000000', 0, '#73d0ff', '#f4cd7c')); 40 | ThemeMap.set('bear', new Theme('#e03c8a', '#bcb28d', '#1f2023', '#000000', 0, '#00aeff', '#e03c8a')); 41 | ThemeMap.set('blue_green', new Theme('#2f97c1', '#0cf574', '#040f0f', '#000000', 0, '#f5b700', '#2f97c1')); 42 | ThemeMap.set('blueberry', new Theme('#82aaff', '#27e8a7', '#242938', '#000000', 0, '#89ddff', '#82aaff')); 43 | ThemeMap.set('buefy', new Theme('#7957d5', '#363636', '#ffffff', '#000000', 0, '#ff3860', '#7957d5')); 44 | ThemeMap.set('calm', new Theme('#e07a5f', '#ebcfb2', '#373f51', '#000000', 0, '#edae49', '#e07a5f')); 45 | ThemeMap.set('chartreuse_dark', new Theme('#7fff00', '#fff', '#000', '#000000', 1, '#00aeff', '#7fff00')); 46 | ThemeMap.set('city_lights', new Theme('#5d8cb3', '#718ca1', '#1d252c', '#000000', 0, '#4798ff', '#5d8cb3')); 47 | ThemeMap.set('cobalt', new Theme('#e683d9', '#75eeb2', '#193549', '#000000', 0, '#0480ef', '#e683d9')); 48 | ThemeMap.set('cobalt2', new Theme('#ffc600', '#0088ff', '#193549', '#000000', 0, '#ffffff', '#ffc600')); 49 | ThemeMap.set('codeSTACKr', new Theme('#ff652f', '#ffffff', '#09131b', '#0c1a25', 1, '#ffe400', '#ff652f')); 50 | ThemeMap.set('darcula', new Theme('#ba5f17', '#bebebe', '#242424', '#000000', 0, '#ffb74d', '#ba5f17')); 51 | ThemeMap.set('dark', new Theme('#fff', '#9f9f9f', '#151515', '#000000', 0, '#79ff97', '#fff')); 52 | ThemeMap.set('date_night', new Theme('#da7885', '#e1b2a2', '#170f0c', '#170f0c', 1, '#bb8470', '#da7885')); 53 | ThemeMap.set('default', new Theme('#586e75', '#586e75', '#ffffff', '#e4e2e2', 1, '#586e75', '#586e75')); 54 | ThemeMap.set('discord_old_blurple', new Theme('#7289da', '#ffffff', '#2c2f33', '#000000', 0, '#7289da', '#7289da')); 55 | ThemeMap.set('dracula', new Theme('#ff79c6', '#ffb86c', '#282a36', '#282a36', 1, '#6272a4', '#bd93f9')); 56 | ThemeMap.set('flag_india', new Theme('#ff8f1c', '#509e2f', '#ffffff', '#000000', 0, '#250e62', '#ff8f1c')); 57 | ThemeMap.set('github_dark', new Theme('#0366d6', '#77909c', '#0d1117', '#2e343b', 1, '#8b949e', '#40c463')); 58 | ThemeMap.set('github', new Theme('#0366d6', '#586069', '#ffffff', '#e4e2e2', 1, '#586069', '#40c463')); 59 | ThemeMap.set('gotham', new Theme('#2aa889', '#99d1ce', '#0c1014', '#000000', 1, '#599cab', '#2aa889')); 60 | ThemeMap.set('graywhite', new Theme('#24292e', '#24292e', '#ffffff', '#000000', 0, '#24292e', '#24292e')); 61 | ThemeMap.set('great_gatsby', new Theme('#ffa726', '#ffd95b', '#000000', '#000000', 0, '#ffb74d', '#ffa726')); 62 | ThemeMap.set('gruvbox', new Theme('#fabd2f', '#8ec07c', '#282828', '#282828', 1, '#fe8019', '#fe8019')); 63 | ThemeMap.set('highcontrast', new Theme('#e7f216', '#fff', '#000', '#000000', 0, '#00ffff', '#e7f216')); 64 | ThemeMap.set('jolly', new Theme('#ff64da', '#ffffff', '#291b3e', '#000000', 0, '#a960ff', '#ff64da')); 65 | ThemeMap.set('kacho_ga', new Theme('#bf4a3f', '#d9c8a9', '#402b23', '#000000', 0, '#a64833', '#bf4a3f')); 66 | ThemeMap.set('maroongold', new Theme('#f7ef8a', '#e0aa3e', '#260000', '#000000', 0, '#f7ef8a', '#f7ef8a')); 67 | ThemeMap.set('material_palenight', new Theme('#c792ea', '#a6accd', '#292d3e', '#000000', 0, '#89ddff', '#c792ea')); 68 | ThemeMap.set('merko', new Theme('#abd200', '#68b587', '#0a0f0b', '#000000', 0, '#b7d364', '#abd200')); 69 | ThemeMap.set('midnight_purple', new Theme('#9745f5', '#ffffff', '#000000', '#000000', 0, '#9f4bff', '#9745f5')); 70 | ThemeMap.set('moltack', new Theme('#86092c', '#574038', '#f5e1c0', '#000000', 0, '#86092c', '#86092c')); 71 | ThemeMap.set('monokai', new Theme('#eb1f6a', '#ffffff', '#2c292d', '#2c292d', 1, '#e28905', '#ae81ff')); 72 | ThemeMap.set('moonlight', new Theme('#ff757f', '#f8f8f8', '#222436', '#222436', 1, '#599dff', '#ff757f')); 73 | ThemeMap.set('nightowl', new Theme('#c792ea', '#7fdbca', '#011627', '#000000', 0, '#ffeb95', '#c792ea')); 74 | ThemeMap.set('noctis_minimus', new Theme('#d3b692', '#c5cdd3', '#1b2932', '#000000', 0, '#72b7c0', '#d3b692')); 75 | ThemeMap.set('nord_bright', new Theme('#3b4252', '#2e3440', '#eceff4', '#e5e9f0', 1, '#8fbcbb', '#88c0d0')); 76 | ThemeMap.set('nord_dark', new Theme('#eceff4', '#e5e9f0', '#2e3440', '#eceff4', 1, '#8fbcbb', '#88c0d0')); 77 | ThemeMap.set('ocean_dark', new Theme('#8957b2', '#92d534', '#151a28', '#000000', 0, '#ffffff', '#8957b2')); 78 | ThemeMap.set('omni', new Theme('#ff79c6', '#e1e1e6', '#191622', '#000000', 0, '#e7de79', '#ff79c6')); 79 | ThemeMap.set('onedark', new Theme('#e4bf7a', '#df6d74', '#282c34', '#000000', 0, '#8eb573', '#e4bf7a')); 80 | ThemeMap.set('outrun', new Theme('#ffcc00', '#8080ff', '#141439', '#000000', 0, '#ff1aff', '#ffcc00')); 81 | ThemeMap.set('panda', new Theme('#19f9d899', '#ff75b5', '#31353a', '#000000', 0, '#19f9d899', '#19f9d899')); 82 | ThemeMap.set('prussian', new Theme('#bddfff', '#6e93b5', '#172f45', '#000000', 0, '#38a0ff', '#bddfff')); 83 | ThemeMap.set('radical', new Theme('#fe428e', '#a9fef7', '#141321', '#141321', 1, '#f8d847', '#ae81ff')); 84 | ThemeMap.set('react', new Theme('#61dafb', '#ffffff', '#20232a', '#000000', 0, '#61dafb', '#61dafb')); 85 | ThemeMap.set('rose_pine', new Theme('#9ccfd8', '#e0def4', '#191724', '#000000', 0, '#ebbcba', '#9ccfd8')); 86 | ThemeMap.set('shades_of_purple', new Theme('#fad000', '#a599e9', '#2d2b55', '#000000', 0, '#b362ff', '#fad000')); 87 | ThemeMap.set('slateorange', new Theme('#faa627', '#ffffff', '#36393f', '#000000', 0, '#faa627', '#faa627')); 88 | ThemeMap.set('solarized_dark', new Theme('#268bd2', '#839496', '#073642', '#073642', 1, '#b58900', '#859900')); 89 | ThemeMap.set('solarized', new Theme('#268bd2', '#586e75', '#fdf6e3', '#fdf6e3', 1, '#b58900', '#859900')); 90 | ThemeMap.set('swift', new Theme('#000000', '#000000', '#f7f7f7', '#000000', 0, '#f05237', '#000000')); 91 | ThemeMap.set('synthwave', new Theme('#e2e9ec', '#e5289e', '#2b213a', '#000000', 0, '#ef8539', '#e2e9ec')); 92 | ThemeMap.set('tokyonight', new Theme('#70a5fd', '#38bdae', '#1a1b27', '#1a1b27', 1, '#bf91f3', '#bf91f3')); 93 | ThemeMap.set('transparent', new Theme('#006AFF', '#417E87', '#00000000', '#000000', 0, '#0579C3', '#006AFF')); 94 | ThemeMap.set('vision_friendly_dark', new Theme('#ffb000', '#ffffff', '#000000', '#000000', 0, '#785ef0', '#ffb000')); 95 | ThemeMap.set('vue', new Theme('#41b883', '#000000', '#ffffff', '#e4e2e2', 1, '#41b883', '#41b883')); 96 | ThemeMap.set('yeblu', new Theme('#ffff00', '#ffffff', '#002046', '#000000', 0, '#ffff00', '#ffff00')); 97 | ThemeMap.set('zenburn', new Theme('#f0dfaf', '#dcdccc', '#3f3f3f', '#3f3f3f', 1, '#8cd0d3', '#7f9f7f')); 98 | -------------------------------------------------------------------------------- /src/github-api/commits-per-language.ts: -------------------------------------------------------------------------------- 1 | import request from '../utils/request'; 2 | 3 | export class CommitLanguageInfo { 4 | name: string; 5 | color: string; // hexadecimal color code 6 | count: number; 7 | 8 | constructor(name: string, color: string = '#586e75', count: number) { 9 | this.name = name; 10 | this.color = color; 11 | this.count = count; 12 | } 13 | } 14 | 15 | export class CommitLanguages { 16 | private languageMap = new Map(); 17 | 18 | public addLanguageCount(name: string, color: string, count: number): void { 19 | if (this.languageMap.has(name)) { 20 | const lang = this.languageMap.get(name)!; 21 | lang.count += count; 22 | this.languageMap.set(name, lang); 23 | } else { 24 | this.languageMap.set(name, new CommitLanguageInfo(name, color, count)); 25 | } 26 | } 27 | 28 | public getLanguageMap(): Map { 29 | return this.languageMap; 30 | } 31 | } 32 | 33 | const fetcher = (token: string, variables: any) => { 34 | return request( 35 | { 36 | Authorization: `bearer ${token}` 37 | }, 38 | { 39 | query: ` 40 | query CommitLanguages($login: String!) { 41 | user(login: $login) { 42 | contributionsCollection { 43 | commitContributionsByRepository(maxRepositories: 100) { 44 | repository { 45 | primaryLanguage { 46 | name 47 | color 48 | } 49 | } 50 | contributions { 51 | totalCount 52 | } 53 | } 54 | } 55 | } 56 | } 57 | `, 58 | variables 59 | } 60 | ); 61 | }; 62 | 63 | // repos per language 64 | export async function getCommitLanguage(username: string, exclude: Array): Promise { 65 | const commitLanguages = new CommitLanguages(); 66 | 67 | const res = await fetcher(process.env.GITHUB_TOKEN!, { 68 | login: username 69 | }); 70 | 71 | if (res.data.errors) { 72 | throw Error(res.data.errors[0].message || 'GetCommitLanguage failed'); 73 | } 74 | 75 | res.data.data.user.contributionsCollection.commitContributionsByRepository.forEach( 76 | (node: { 77 | repository: {primaryLanguage: {name: string; color: string} | null}; 78 | contributions: {totalCount: number}; 79 | }) => { 80 | if (node.repository.primaryLanguage == null) { 81 | return; 82 | } 83 | const langName = node.repository.primaryLanguage.name; 84 | const langColor = node.repository.primaryLanguage.color; 85 | const totalCount = node.contributions.totalCount; 86 | if (!exclude.includes(langName.toLowerCase())) { 87 | commitLanguages.addLanguageCount(langName, langColor, totalCount); 88 | } 89 | } 90 | ); 91 | 92 | return commitLanguages; 93 | } 94 | -------------------------------------------------------------------------------- /src/github-api/contributions-by-year.ts: -------------------------------------------------------------------------------- 1 | import request from '../utils/request'; 2 | 3 | export class ConrtibutionByYear { 4 | year: number; 5 | totalCommitContributions: number; 6 | totalContributions: number; 7 | constructor(year: number, totalCommitContributions: number, totalContributions: number) { 8 | this.year = year; 9 | this.totalCommitContributions = totalCommitContributions; 10 | this.totalContributions = totalContributions; 11 | } 12 | } 13 | 14 | const fetcher = (token: string, variables: any, year: number) => { 15 | return request( 16 | { 17 | Authorization: `bearer ${token}` 18 | }, 19 | { 20 | query: ` 21 | query ContributionsByYear($login: String!) { 22 | user(login: $login) { 23 | ${ 24 | year 25 | ? `contributionsCollection(from: "${year}-01-01T00:00:00Z", to: "${year}-12-31T23:59:59Z") {` 26 | : 'contributionsCollection {' 27 | } 28 | totalCommitContributions 29 | contributionCalendar { 30 | totalContributions 31 | } 32 | } 33 | } 34 | } 35 | `, 36 | variables 37 | } 38 | ); 39 | }; 40 | 41 | export async function getContributionByYear(username: string, year: number): Promise { 42 | const res = await fetcher( 43 | process.env.GITHUB_TOKEN!, 44 | { 45 | login: username 46 | }, 47 | year 48 | ); 49 | 50 | if (res.data.errors) { 51 | throw Error(res.data.errors[0].message || 'GetContributionByYear failed'); 52 | } 53 | 54 | const user = res.data.data.user; 55 | 56 | const result = new ConrtibutionByYear( 57 | year, 58 | user.contributionsCollection.totalCommitContributions, 59 | user.contributionsCollection.contributionCalendar.totalContributions 60 | ); 61 | return result; 62 | } 63 | -------------------------------------------------------------------------------- /src/github-api/productive-time.ts: -------------------------------------------------------------------------------- 1 | import request from '../utils/request'; 2 | 3 | export class ProfuctiveTime { 4 | productiveDate: Date[] = []; 5 | 6 | public addProductiveDate(date: Date) { 7 | this.productiveDate.push(date); 8 | } 9 | } 10 | 11 | const userIdFetcher = (token: string, variables: any) => { 12 | return request( 13 | { 14 | Authorization: `bearer ${token}` 15 | }, 16 | { 17 | query: ` 18 | query getUserId($login: String!) { 19 | user(login: $login) { 20 | id 21 | } 22 | } 23 | `, 24 | variables 25 | } 26 | ); 27 | }; 28 | 29 | // We use commit datetime to calculate productive time 30 | const fetcher = (token: string, variables: any) => { 31 | return request( 32 | { 33 | Authorization: `bearer ${token}` 34 | }, 35 | { 36 | query: ` 37 | query ProductiveTime($login: String!,$userId: ID!,$until: GitTimestamp!,,$since: GitTimestamp!) { 38 | user(login: $login) { 39 | contributionsCollection{ 40 | commitContributionsByRepository(maxRepositories:50) { 41 | repository { 42 | defaultBranchRef { 43 | target { 44 | ... on Commit { 45 | history(first: 50,since: $since,until: $until,author:{id:$userId}) { 46 | edges { 47 | node { 48 | message 49 | author{ 50 | email 51 | } 52 | committedDate 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | name 60 | } 61 | } 62 | } 63 | } 64 | } 65 | `, 66 | variables 67 | } 68 | ); 69 | }; 70 | 71 | // get productive time 72 | export async function getProductiveTime(username: string, until: string, since: string): Promise { 73 | const userIdResponse = await userIdFetcher(process.env.GITHUB_TOKEN!, { 74 | login: username 75 | }); 76 | 77 | if (userIdResponse.data.errors) { 78 | throw Error(userIdResponse.data.errors[0].message || 'GetProductiveTime failed'); 79 | } 80 | 81 | const userId = userIdResponse.data.data.user.id; 82 | const res = await fetcher(process.env.GITHUB_TOKEN!, { 83 | login: username, 84 | userId: userId, 85 | until: until, 86 | since: since 87 | }); 88 | 89 | if (res.data.errors) { 90 | throw Error(res.data.errors[0].message || 'GetProductiveTime failed'); 91 | } 92 | 93 | const productiveTime = new ProfuctiveTime(); 94 | res.data.data.user.contributionsCollection.commitContributionsByRepository.forEach( 95 | (node: { 96 | repository: { 97 | defaultBranchRef: {target: {history: {edges: any[]}}} | null; 98 | }; 99 | }) => { 100 | if (node.repository.defaultBranchRef != null) { 101 | node.repository.defaultBranchRef.target.history.edges.forEach(node => { 102 | productiveTime.addProductiveDate(node.node.committedDate); 103 | }); 104 | } 105 | } 106 | ); 107 | 108 | return productiveTime; 109 | } 110 | -------------------------------------------------------------------------------- /src/github-api/profile-details.ts: -------------------------------------------------------------------------------- 1 | import request from '../utils/request'; 2 | 3 | export class ProfileDetails { 4 | id: number; // user id 5 | name: string; 6 | email: string; 7 | createdAt: string; 8 | company: string | null = null; 9 | websiteUrl: string | null = null; 10 | twitterUsername: string | null = null; 11 | location: string | null = null; 12 | totalPublicRepos: number = 0; 13 | totalStars: number = 0; 14 | totalIssueContributions: number = 0; 15 | totalPullRequestContributions: number = 0; 16 | totalRepositoryContributions: number = 0; 17 | contributions: ProfileContribution[] = []; 18 | contributionYears: number[] = []; 19 | constructor(id: number, name: string, email: string, createdAt: string) { 20 | this.id = id; 21 | this.name = name; 22 | this.email = email; 23 | this.createdAt = createdAt; 24 | } 25 | } 26 | 27 | export class ProfileContribution { 28 | contributionCount: number = 0; 29 | date: Date; 30 | constructor(date: Date, count: number) { 31 | this.date = date; 32 | this.contributionCount = count; 33 | } 34 | } 35 | 36 | const fetcher = (token: string, variables: any) => { 37 | // contain private need token permission 38 | // contributionsCollection default to a year ago 39 | return request( 40 | { 41 | Authorization: `bearer ${token}` 42 | }, 43 | { 44 | query: ` 45 | query UserDetails($login: String!) { 46 | user(login: $login) { 47 | id 48 | name 49 | email 50 | createdAt 51 | twitterUsername 52 | company 53 | location 54 | websiteUrl 55 | repositories(first: 100,privacy:PUBLIC, isFork: false, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}) { 56 | totalCount 57 | nodes { 58 | stargazers { 59 | totalCount 60 | } 61 | } 62 | } 63 | contributionsCollection { 64 | contributionCalendar { 65 | weeks { 66 | contributionDays { 67 | contributionCount 68 | date 69 | } 70 | } 71 | } 72 | contributionYears 73 | } 74 | repositoriesContributedTo(first: 1,includeUserRepositories:true, privacy:PUBLIC, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) { 75 | totalCount 76 | } 77 | pullRequests(first: 1) { 78 | totalCount 79 | } 80 | issues(first: 1) { 81 | totalCount 82 | } 83 | } 84 | } 85 | 86 | `, 87 | variables 88 | } 89 | ); 90 | }; 91 | 92 | export async function getProfileDetails(username: string): Promise { 93 | const res = await fetcher(process.env.GITHUB_TOKEN!, { 94 | login: username 95 | }); 96 | 97 | if (res.data.errors) { 98 | throw Error(res.data.errors[0].message || 'GetProfileDetails failed'); 99 | } 100 | 101 | const user = res.data.data.user; 102 | const profileDetails = new ProfileDetails(user.id, user.name, user.email, user.createdAt); 103 | profileDetails.totalPublicRepos = user.repositories.totalCount; 104 | profileDetails.totalStars = user.repositories.nodes.reduce( 105 | (stars: number, curr: {stargazers: {totalCount: number}}) => { 106 | return stars + curr.stargazers.totalCount; 107 | }, 108 | 0 109 | ); 110 | profileDetails.websiteUrl = user.websiteUrl; 111 | profileDetails.totalIssueContributions = user.issues.totalCount; 112 | profileDetails.totalPullRequestContributions = user.pullRequests.totalCount; 113 | profileDetails.totalRepositoryContributions = user.repositoriesContributedTo.totalCount; 114 | profileDetails.company = user.company; 115 | profileDetails.location = user.location; 116 | profileDetails.twitterUsername = user.twitterUsername; 117 | profileDetails.contributionYears = user.contributionsCollection.contributionYears; 118 | 119 | // contributions into array 120 | for (const week of user.contributionsCollection.contributionCalendar.weeks) { 121 | for (const day of week.contributionDays) { 122 | profileDetails.contributions.push(new ProfileContribution(new Date(day.date), day.contributionCount)); 123 | } 124 | } 125 | 126 | return profileDetails; 127 | } 128 | -------------------------------------------------------------------------------- /src/github-api/repos-per-language.ts: -------------------------------------------------------------------------------- 1 | import request from '../utils/request'; 2 | 3 | export class RepoLanguageInfo { 4 | name: string; 5 | color: string; // hexadecimal color code 6 | count: number; 7 | 8 | constructor(name: string, color: string = '#586e75', count: number) { 9 | this.name = name; 10 | this.color = color; 11 | this.count = count; 12 | } 13 | } 14 | 15 | export class RepoLanguages { 16 | private languageMap = new Map(); 17 | 18 | public addLanguage(name: string, color: string): void { 19 | if (this.languageMap.has(name)) { 20 | const lang = this.languageMap.get(name)!; 21 | lang.count += 1; 22 | this.languageMap.set(name, lang); 23 | } else { 24 | this.languageMap.set(name, new RepoLanguageInfo(name, color, 1)); 25 | } 26 | } 27 | 28 | public getLanguageMap(): Map { 29 | return this.languageMap; 30 | } 31 | } 32 | 33 | const fetcher = (token: string, variables: any) => { 34 | // contain private repo need token permission 35 | return request( 36 | { 37 | Authorization: `bearer ${token}` 38 | }, 39 | { 40 | query: ` 41 | query ReposPerLanguage($login: String!,$endCursor: String) { 42 | user(login: $login) { 43 | repositories(isFork: false, first: 100, after: $endCursor,ownerAffiliations: OWNER) { 44 | nodes { 45 | primaryLanguage { 46 | name 47 | color 48 | } 49 | } 50 | pageInfo{ 51 | endCursor 52 | hasNextPage 53 | } 54 | } 55 | } 56 | } 57 | `, 58 | variables 59 | } 60 | ); 61 | }; 62 | 63 | // repos per language 64 | export async function getRepoLanguages(username: string, exclude: Array): Promise { 65 | let hasNextPage = true; 66 | let cursor = null; 67 | const repoLanguages = new RepoLanguages(); 68 | const nodes = []; 69 | 70 | while (hasNextPage) { 71 | const res: any = await fetcher(process.env.GITHUB_TOKEN!, { 72 | login: username, 73 | endCursor: cursor 74 | }); 75 | 76 | if (res.data.errors) { 77 | throw Error(res.data.errors[0].message || 'GetRepoLanguage fail'); 78 | } 79 | cursor = res.data.data.user.repositories.pageInfo.endCursor; 80 | hasNextPage = res.data.data.user.repositories.pageInfo.hasNextPage; 81 | nodes.push(...res.data.data.user.repositories.nodes); 82 | } 83 | 84 | nodes.forEach(node => { 85 | if (node.primaryLanguage) { 86 | const langName = node.primaryLanguage.name; 87 | const langColor = node.primaryLanguage.color; 88 | if (!exclude.includes(langName.toLowerCase())) { 89 | repoLanguages.addLanguage(langName, langColor); 90 | } 91 | } 92 | }); 93 | 94 | return repoLanguages; 95 | } 96 | -------------------------------------------------------------------------------- /src/templates/card.ts: -------------------------------------------------------------------------------- 1 | import {Theme} from '../const/theme'; 2 | import * as d3 from 'd3'; 3 | import {JSDOM} from 'jsdom'; 4 | export class Card { 5 | title: string; 6 | width: number; 7 | height: number; 8 | xPadding: number; 9 | yPadding: number; 10 | body: d3.Selection; 11 | svg: d3.Selection; 12 | constructor(title = 'Title', width = 1280, height = 1024, theme: Theme, xPadding = 30, yPadding = 40) { 13 | this.title = title; 14 | this.width = width; 15 | this.height = height; 16 | this.xPadding = xPadding; 17 | this.yPadding = yPadding; 18 | // use fake dom let us can get html element 19 | const fakeDom = new JSDOM(''); 20 | this.body = d3.select(fakeDom.window.document).select('body'); 21 | this.svg = this.body 22 | .append('div') 23 | .attr('class', 'container') 24 | .append('svg') 25 | .attr('xmlns', 'http://www.w3.org/2000/svg') 26 | .attr('width', width) 27 | .attr('height', height) 28 | .attr('viewBox', `0 0 ${this.width} ${this.height}`); 29 | this.svg.append('style').html( 30 | `* { 31 | font-family: 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif 32 | }` 33 | ); 34 | const strokeWidth = 1; 35 | this.svg 36 | .append('rect') 37 | .attr('x', 1) 38 | .attr('y', 1) 39 | .attr('rx', 5) 40 | .attr('ry', 5) 41 | // 100% - 2px to show borderline 42 | .attr('height', `${((height - 2 * strokeWidth) / height) * 100}%`) 43 | // 100% - 2px to show borderline 44 | .attr('width', `${((width - 2 * strokeWidth) / width) * 100}%`) 45 | .attr('stroke', `${theme.stroke}`) 46 | .attr('stroke-width', strokeWidth) 47 | .attr('fill', `${theme.background}`) 48 | .attr('stroke-opacity', `${theme.strokeOpacity}`); 49 | 50 | const isEmptyTitle = this.title == ''; 51 | if (!isEmptyTitle) { 52 | this.svg 53 | .append('text') 54 | .attr('x', this.xPadding) 55 | .attr('y', this.yPadding) 56 | .style('font-size', `22px`) 57 | .style('fill', `${theme.title}`) 58 | .text(this.title); 59 | } 60 | this.svg = this.svg.append('g').attr('transform', 'translate(0,40)'); 61 | } 62 | 63 | getSVG() { 64 | return this.svg; 65 | } 66 | 67 | toString() { 68 | return this.body.select('.container').html(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/templates/donut-chart-card.ts: -------------------------------------------------------------------------------- 1 | import {Card} from './card'; 2 | import * as d3 from 'd3'; 3 | import {PieArcDatum} from 'd3-shape'; 4 | import {Theme} from '../const/theme'; 5 | 6 | export function createDonutChartCard( 7 | title: string, 8 | data: {name: string; value: number; color: string}[], 9 | theme: Theme 10 | ) { 11 | const pie = d3.pie<{name: string; value: number; color: string}>().value(function (d) { 12 | return d.value; 13 | }); 14 | const pieData = pie(data); 15 | const card = new Card(title, 340, 200, theme); 16 | 17 | const margin = 10; 18 | const radius = (Math.min(card.width, card.height) - 2 * margin - card.yPadding) / 2; 19 | 20 | const arc = d3 21 | .arc>() 22 | .outerRadius(radius - 10) 23 | .innerRadius(radius / 2); 24 | 25 | const svg = card.getSVG(); 26 | // draw language node 27 | 28 | const panel = svg.append('g').attr('transform', `translate(${card.xPadding + margin},${0})`); 29 | const labelHeight = 14; 30 | panel 31 | .selectAll(null) 32 | .data(pieData) 33 | .enter() 34 | .append('rect') 35 | .attr('y', d => labelHeight * d.index * 1.8 + card.height / 2 - radius - 12) // rect y-coordinate need fix,so I decrease y, but I don't know why this need fix. 36 | .attr('width', labelHeight) 37 | .attr('height', labelHeight) 38 | .attr('fill', pieData => pieData.data.color) 39 | .attr('stroke', `${theme.background}`) 40 | .style('stroke-width', '1px'); 41 | 42 | // set language text 43 | panel 44 | .selectAll(null) 45 | .data(pieData) 46 | .enter() 47 | .append('text') 48 | .text(d => { 49 | return d.data.name; 50 | }) 51 | .attr('x', labelHeight * 1.2) 52 | .attr('y', d => labelHeight * d.index * 1.8 + card.height / 2 - radius) 53 | .style('fill', theme.text) 54 | .style('font-size', `${labelHeight}px`); 55 | 56 | // draw pie chart 57 | const g = svg 58 | .append('g') 59 | .attr( 60 | 'transform', 61 | `translate( ${card.width - radius - margin - card.xPadding}, ${(card.height - card.yPadding) / 2} )` 62 | ) 63 | .selectAll('.arc') 64 | .data(pieData) 65 | .enter() 66 | .append('g') 67 | .attr('class', 'arc'); 68 | 69 | g.append('path') 70 | .attr('d', arc) 71 | .style('fill', function (pieData) { 72 | return pieData.data.color; 73 | }) 74 | .attr('stroke', `${theme.background}`) 75 | .style('stroke-width', '2px'); 76 | return card.toString(); 77 | } 78 | -------------------------------------------------------------------------------- /src/templates/productive-time-card.ts: -------------------------------------------------------------------------------- 1 | import {Card} from './card'; 2 | import {Theme} from '../const/theme'; 3 | import * as d3 from 'd3'; 4 | import * as d3Axis from 'd3-axis'; 5 | 6 | export function createProductiveCard(chartData: number[], theme: Theme, utcOffset: number) { 7 | const title = 'Commits ' + '(UTC ' + (utcOffset >= 0 ? '+' : '') + utcOffset.toFixed(2) + ')'; 8 | const card = new Card(title, 340, 200, theme); 9 | const svg = card.getSVG(); 10 | 11 | const chartWidth = card.width - 60; 12 | const chartHeight = 100; 13 | const bottomScaleBand = d3.scaleBand().range([0, chartWidth]).padding(0.1); 14 | const bottomAxis: d3Axis.Axis = d3Axis.axisBottom(bottomScaleBand); 15 | 16 | if (chartData.length != 24) { 17 | throw Error('productive time array size should be 24'); 18 | } 19 | 20 | bottomScaleBand.domain( 21 | chartData.map(function (_: number, index: number, __: number[]) { 22 | return index; 23 | }) 24 | ); 25 | 26 | const yMax = Math.max( 27 | ...chartData.map(function (d: number) { 28 | return d; 29 | }) 30 | ); 31 | 32 | const y = d3.scaleLinear().range([chartHeight, 0]); 33 | y.domain([0, yMax]); 34 | y.nice(); 35 | 36 | const chartPanel = svg 37 | .append('g') 38 | .attr('color', theme.chart) 39 | .attr('transform', `translate(${(card.width - chartWidth) / 2 + 5},${card.yPadding / 2})`); 40 | const xAxis = bottomAxis.tickValues([0, 6, 12, 18, 23]); 41 | 42 | // Add the X Axis 43 | const g = chartPanel.append('g').attr('color', theme.text).attr('transform', `translate(0,${chartHeight})`); 44 | g.call(xAxis); 45 | 46 | // custom x axis, here is svg magic 47 | // Add more space for first bar 48 | g.select('.domain').attr( 49 | 'd', 50 | `M${0 - bottomScaleBand(1)! + bottomScaleBand(0)! + bottomScaleBand.bandwidth()},0.5H${chartWidth}.5` 51 | ); 52 | 53 | // Add the Y Axis 54 | chartPanel 55 | .append('g') 56 | .attr('color', theme.text) 57 | // Add gap before first bar 58 | .attr( 59 | 'transform', 60 | `translate(${0 - bottomScaleBand(1)! + bottomScaleBand(0)! + bottomScaleBand.bandwidth()},0)` 61 | ) 62 | .call(d3.axisLeft(y).ticks(5)); 63 | 64 | chartPanel 65 | .selectAll('.bar') 66 | .data(chartData) 67 | .enter() 68 | .append('rect') 69 | .attr('class', 'bar') 70 | .style('hover', 'green') 71 | .attr('fill', theme.chart) 72 | .attr('x', function (_, index) { 73 | return bottomScaleBand(index)!; 74 | }) 75 | .attr('y', function (d) { 76 | return y(Number(d)); 77 | }) 78 | .attr('width', bottomScaleBand.bandwidth()) 79 | .attr('height', function (d) { 80 | return chartHeight - y(Number(d)); 81 | }); 82 | 83 | chartPanel 84 | .append('g') 85 | .append('text') 86 | .text('per day hour') 87 | .attr('y', 130) 88 | .attr('x', 220) 89 | .style('fill', theme.text) 90 | .style('font-size', `10px`); 91 | 92 | return card.toString(); 93 | } 94 | -------------------------------------------------------------------------------- /src/templates/profile-details-card.ts: -------------------------------------------------------------------------------- 1 | import {Card} from './card'; 2 | import * as d3 from 'd3'; 3 | import moment from 'moment'; 4 | import {Theme} from '../const/theme'; 5 | 6 | export function createDetailCard( 7 | title: string, 8 | userDetails: { 9 | index: number; 10 | icon: string; 11 | name: string; 12 | value: string; 13 | }[], 14 | contributionsData: {contributionCount: number; date: Date}[], 15 | theme: Theme 16 | ) { 17 | const card = new Card(title, 700, 200, theme); 18 | const svg = card.getSVG(); 19 | 20 | // draw icon 21 | const panel = svg.append('g').attr('transform', `translate(30,30)`); 22 | const labelHeight = 14; 23 | panel 24 | .selectAll(null) 25 | .data(userDetails) 26 | .enter() 27 | .append('g') 28 | .attr('transform', d => { 29 | const y = labelHeight * d.index * 2; 30 | return `translate(0,${y})`; 31 | }) 32 | .attr('width', labelHeight) 33 | .attr('height', labelHeight) 34 | .attr('fill', theme.icon) 35 | .html(d => d.icon); 36 | 37 | // draw text 38 | panel 39 | .selectAll(null) 40 | .data(userDetails) 41 | .enter() 42 | .append('text') 43 | .text(d => { 44 | return d.value; 45 | }) 46 | .attr('x', labelHeight * 1.5) 47 | .attr('y', d => labelHeight * d.index * 2 + labelHeight) 48 | .style('fill', theme.text) 49 | .style('font-size', `${labelHeight}px`); 50 | 51 | // process chart data 52 | const lineChartData: {contributionCount: number; date: Date}[] = []; 53 | for (const data of contributionsData) { 54 | const formatDate = moment(data.date).format('YYYY-MM'); 55 | data.date = new Date(formatDate); 56 | const lastIndex = lineChartData.length - 1; 57 | if (lineChartData.length == 0 || lineChartData[lastIndex].date.getTime() !== data.date.getTime()) { 58 | lineChartData.push({ 59 | contributionCount: data.contributionCount, 60 | date: data.date 61 | }); // use new object 62 | } else { 63 | lineChartData[lastIndex].contributionCount += data.contributionCount; 64 | } 65 | } 66 | 67 | // prepare chart data 68 | const chartRightMargin = 30; 69 | const chartWidth = card.width - 2 * card.xPadding - chartRightMargin - 230; 70 | const chartHeight = card.height - 2 * card.yPadding - 10; 71 | const x = d3.scaleTime().range([0, chartWidth]); 72 | 73 | x.domain( 74 | <[Date, Date]>d3.extent(lineChartData, function (d) { 75 | return d.date; 76 | }) 77 | ); 78 | 79 | // eslint-disable-next-line prefer-spread 80 | const yMax = Math.max.apply( 81 | Math, 82 | lineChartData.map(function (o) { 83 | return o.contributionCount; 84 | }) 85 | ); 86 | 87 | const y = d3.scaleLinear().range([chartHeight, 0]); 88 | y.domain([0, yMax]); 89 | y.nice(); 90 | 91 | const valueline = d3 92 | .area<{contributionCount: number; date: Date}>() 93 | .x(function (d) { 94 | return x(d.date); 95 | }) 96 | .y0(y(0)) 97 | .y1(function (d) { 98 | return y(d.contributionCount); 99 | }) 100 | .curve(d3.curveMonotoneX); 101 | 102 | const chartPanel = svg 103 | .append('g') 104 | .attr('color', theme.chart) 105 | .attr('transform', `translate(${card.width - chartWidth - card.xPadding + 5},10)`); 106 | 107 | // draw chart line 108 | chartPanel 109 | .append('path') 110 | .data([lineChartData]) 111 | .attr('transform', `translate(${-chartRightMargin},0)`) 112 | .attr('stroke', theme.chart) 113 | .attr('fill', theme.chart) 114 | .attr('opacity', 1) 115 | .attr('d', valueline); 116 | 117 | // Add the X Axis 118 | const xAxis = d3 119 | .axisBottom(x) 120 | .tickFormat(d3.timeFormat('%y/%m')) 121 | .tickValues( 122 | lineChartData 123 | .filter((_, i) => { 124 | return i % 2 === 0; 125 | }) 126 | .map(d => { 127 | return d.date; 128 | }) 129 | ); 130 | 131 | chartPanel 132 | .append('g') 133 | .attr('color', theme.text) 134 | .attr('transform', `translate(${-chartRightMargin},${chartHeight})`) 135 | .call(xAxis); 136 | 137 | // Add the Y Axis 138 | chartPanel 139 | .append('g') 140 | .attr('color', theme.text) 141 | .attr('transform', `translate(${chartWidth - chartRightMargin},0)`) 142 | .call(d3.axisRight(y).ticks(8)); 143 | 144 | // hard code this coordinate becuz I'm too lazy 145 | chartPanel 146 | .append('g') 147 | .append('text') 148 | .text('contributions in the last year') 149 | .attr('y', title.length > 30 ? 140 : -15) // if the title is too long, then put text to the bottom 150 | .attr('x', 230) 151 | .style('fill', theme.text) 152 | .style('font-size', `10px`); 153 | 154 | return card.toString(); 155 | } 156 | -------------------------------------------------------------------------------- /src/templates/stats-card.ts: -------------------------------------------------------------------------------- 1 | import {Card} from './card'; 2 | import {Icon} from '../const/icon'; 3 | import {Theme} from '../const/theme'; 4 | 5 | export function createStatsCard( 6 | title: string, 7 | statsData: {index: number; icon: string; name: string; value: string}[], 8 | theme: Theme 9 | ) { 10 | const card = new Card(title, 340, 200, theme); 11 | const svg = card.getSVG(); 12 | 13 | // draw icon 14 | const panel = svg.append('g').attr('transform', `translate(30,20)`); 15 | const labelHeight = 14; 16 | panel 17 | .selectAll(null) 18 | .data(statsData) 19 | .enter() 20 | .append('g') 21 | .attr('transform', d => { 22 | const y = labelHeight * d.index * 1.8; 23 | return `translate(0,${y})`; 24 | }) 25 | .attr('width', labelHeight) 26 | .attr('height', labelHeight) 27 | .attr('fill', theme.icon) 28 | .html(d => d.icon); 29 | 30 | // draw text 31 | panel 32 | .selectAll(null) 33 | .data(statsData) 34 | .enter() 35 | .append('text') 36 | .text(d => { 37 | return `${d.name}`; 38 | }) 39 | .attr('x', labelHeight * 1.5) 40 | .attr('y', d => labelHeight * d.index * 1.8 + labelHeight) 41 | .style('fill', theme.text) 42 | .style('font-size', `${labelHeight}px`); 43 | 44 | panel 45 | .selectAll(null) 46 | .data(statsData) 47 | .enter() 48 | .append('text') 49 | .text(d => { 50 | return `${d.value}`; 51 | }) 52 | .attr('x', 130) 53 | .attr('y', d => labelHeight * d.index * 1.8 + labelHeight) 54 | .style('fill', theme.text) 55 | .style('font-size', `${labelHeight}px`); 56 | 57 | const panelForGitHubLogo = svg.append('g').attr('transform', `translate(220,20)`); 58 | panelForGitHubLogo.append('g').attr('transform', `scale(6)`).style('fill', theme.icon).html(Icon.GITHUB); 59 | 60 | return card.toString(); 61 | } 62 | -------------------------------------------------------------------------------- /src/utils/file-writer.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import {mkdirSync, writeFileSync, readdirSync} from 'fs'; 3 | import {ThemeMap} from '../const/theme'; 4 | 5 | export const OUTPUT_PATH = './profile-summary-card-output/'; 6 | const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY; 7 | 8 | // If neither a branch or tag is available for the event type, the variable will not exist. https://docs.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables 9 | const GITHUB_BRANCH = 10 | process.env.GITHUB_REF == undefined 11 | ? core.getInput('BRANCH_NAME', {required: false}) 12 | : process.env.GITHUB_REF.split('/').pop(); 13 | 14 | export const writeSVG = function (folder: string, filename: string, svgString: string) { 15 | const targetFolder = `${OUTPUT_PATH}${folder}/`; 16 | mkdirSync(targetFolder, {recursive: true}); 17 | writeFileSync(`${targetFolder}${filename}.svg`, svgString); 18 | }; 19 | 20 | function getAllFileInFolder(folder: string): string[] { 21 | const files: string[] = []; 22 | readdirSync(folder).forEach(file => { 23 | files.push(file); 24 | }); 25 | return files; 26 | } 27 | 28 | export const generatePreviewMarkdown = function (isInGithubAction: boolean) { 29 | const targetFolder = `${OUTPUT_PATH}`; 30 | let readmeContent = ''; 31 | const urlPrefix = isInGithubAction 32 | ? `https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/${GITHUB_BRANCH}/profile-summary-card-output` 33 | : `.`; 34 | 35 | // First, we generate preview readme for each theme 36 | for (const themeName of ThemeMap.keys()) { 37 | generateThemePreviewReadme(urlPrefix, themeName); 38 | } 39 | readmeContent += ` 40 | # Theme Preview 41 | 42 | Here are all cards with themes. 43 | | :bell: | If only show Top Languages card here, then you maybe forgot to use [Personal access token](https://docs.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets) instead of GITHUB_TOKEN in workflow. | 44 | | :-------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 45 | 46 | `; 47 | 48 | for (const themeName of ThemeMap.keys()) { 49 | readmeContent += `## [${themeName}](./${themeName}/README.md)`; 50 | readmeContent += getThemeMarkdown(`${urlPrefix}/${themeName}`); 51 | } 52 | 53 | writeFileSync(`${targetFolder}README.md`, readmeContent); 54 | }; 55 | 56 | function generateThemePreviewReadme(urlPrefix: string, themeName: string) { 57 | let themePreviewMarkdown = ''; 58 | themePreviewMarkdown += `## ${themeName}`; 59 | themePreviewMarkdown += `\n`; 60 | themePreviewMarkdown += getThemeMarkdown('.'); 61 | themePreviewMarkdown += '### Now you can add this to your markdown'; 62 | themePreviewMarkdown += ` 63 | \`\`\` 64 | ${getThemeMarkdown(`${urlPrefix}/${themeName}`)} 65 | \`\`\` 66 | `; 67 | themePreviewMarkdown += `\n`; 68 | themePreviewMarkdown += `### Each card usage`; 69 | for (const file of getAllFileInFolder(OUTPUT_PATH + themeName)) { 70 | if (!file.endsWith('svg')) continue; 71 | themePreviewMarkdown += ` 72 | --- 73 | 74 | ![](./${file}) 75 | 76 | \`\`\` 77 | ![](${urlPrefix}/${themeName}/${file}) 78 | \`\`\` 79 | 80 | `; 81 | themePreviewMarkdown += `\n`; 82 | } 83 | writeFileSync(`${OUTPUT_PATH}${themeName}/README.md`, themePreviewMarkdown); 84 | } 85 | 86 | function getThemeMarkdown(urlPrefix: string): string { 87 | let result = ''; 88 | result += ` 89 | [![](${urlPrefix}/0-profile-details.svg)](https://github.com/vn7n24fzkq/github-profile-summary-cards) 90 | [![](${urlPrefix}/1-repos-per-language.svg)](https://github.com/vn7n24fzkq/github-profile-summary-cards) [![](${urlPrefix}/2-most-commit-language.svg)](https://github.com/vn7n24fzkq/github-profile-summary-cards) 91 | [![](${urlPrefix}/3-stats.svg)](https://github.com/vn7n24fzkq/github-profile-summary-cards) [![](${urlPrefix}/4-productive-time.svg)](https://github.com/vn7n24fzkq/github-profile-summary-cards) 92 | `; 93 | return result; 94 | } 95 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import core from '@actions/core'; 2 | import rax from 'retry-axios'; 3 | import axios, {AxiosPromise} from 'axios'; 4 | 5 | rax.attach(); 6 | 7 | export default function request(header: any, data: any): AxiosPromise { 8 | return axios({ 9 | url: 'https://api.github.com/graphql', 10 | method: 'post', 11 | headers: header, 12 | data: data, 13 | raxConfig: { 14 | retry: 10, 15 | noResponseRetries: 3, 16 | retryDelay: 1000, 17 | backoffType: 'linear', 18 | httpMethodsToRetry: ['POST'], 19 | onRetryAttempt: err => { 20 | const cfg = rax.getConfig(err); 21 | core.warning(err); 22 | core.warning(`Retry attempt #${cfg?.currentRetryAttempt}`); 23 | } 24 | } 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/translator.ts: -------------------------------------------------------------------------------- 1 | export const translateLanguage = function async(lang: string) { 2 | // this is a list of all Github supported languages 3 | // that have known aliases 4 | // aliases with non URL safe characters have been removed 5 | // https://github.com/github/linguist/blob/master/lib/linguist/languages.yml 6 | // some keys added (cs: C#) etc. 7 | const dict: {[id: string]: string} = { 8 | ags: 'AGS Script', 9 | aspx: 'ASP.NET', 10 | ats2: 'ATS', 11 | actionscript3: 'ActionScript', 12 | as3: 'ActionScript', 13 | ada95: 'Ada', 14 | ada2005: 'Ada', 15 | acfm: 'Adobe Font Metrics', 16 | amfm: 'Adobe Font Metrics', 17 | abuild: 'Alpine Abuild', 18 | apkbuild: 'Alpine Abuild', 19 | altium: 'Altium Designer', 20 | aconf: 'ApacheConf', 21 | apache: 'ApacheConf', 22 | osascript: 'AppleScript', 23 | asm: 'Assembly', 24 | nasm: 'Assembly', 25 | ahk: 'AutoHotkey', 26 | au3: 'AutoIt', 27 | AutoIt3: 'AutoIt', 28 | AutoItScript: 'AutoIt', 29 | bat: 'Batchfile', 30 | batch: 'Batchfile', 31 | dosbatch: 'Batchfile', 32 | winbatch: 'Batchfile', 33 | be: 'Berry', 34 | b3d: 'BlitzBasic', 35 | blitz3d: 'BlitzBasic', 36 | blitzplus: 'BlitzBasic', 37 | bplus: 'BlitzBasic', 38 | bmax: 'BlitzMax', 39 | csharp: 'C#', 40 | cs: 'C#', 41 | cake: 'C#', 42 | cakescript: 'C#', 43 | cpp: 'C++', 44 | c2hs: 'C2hs Haskell', 45 | cds: 'CAP CDS', 46 | Cabal: 'Cabal Config', 47 | Carto: 'CartoCSS', 48 | chpl: 'Chapel', 49 | checksum: 'Checksums', 50 | hash: 'Checksums', 51 | hashes: 'Checksums', 52 | sum: 'Checksums', 53 | sums: 'Checksums', 54 | asp: 'Classic ASP', 55 | soy: 'Closure Templates', 56 | CoNLL: 'CoNLL-U', 57 | ql: 'CodeQL', 58 | coffee: 'CoffeeScript', 59 | cfm: 'ColdFusion', 60 | cfml: 'ColdFusion', 61 | cfc: 'ColdFusion CFC', 62 | lisp: 'Common Lisp', 63 | cwl: 'Common Workflow Language', 64 | pyrex: 'Cython', 65 | Dlang: 'D', 66 | dcl: 'DIGITAL Command Language', 67 | byond: 'DM', 68 | dpatch: 'Darcs Patch', 69 | udiff: 'Diff', 70 | Containerfile: 'Dockerfile', 71 | email: 'E-mail', 72 | eml: 'E-mail', 73 | mail: 'E-mail', 74 | mbox: 'E-mail', 75 | Earthfile: 'Earthly', 76 | elisp: 'Emacs Lisp', 77 | emacs: 'Emacs Lisp', 78 | fsharp: 'F#', 79 | fstar: 'F*', 80 | FIGfont: 'FIGlet Font', 81 | fb: 'FreeBasic', 82 | ftl: 'FreeMarker', 83 | pot: 'Gettext Catalog', 84 | cucumber: 'Gherkin', 85 | gitattributes: 'Git Attributes', 86 | gitconfig: 'Git Config', 87 | gitmodules: 'Git Config', 88 | golang: 'Go', 89 | gf: 'Grammatical Framework', 90 | gsp: 'Groovy Server Pages', 91 | terraform: 'HCL', 92 | xhtml: 'HTML', 93 | ecr: 'HTML+ECR', 94 | eex: 'HTML+EEX', 95 | heex: 'HTML+EEX', 96 | leex: 'HTML+EEX', 97 | erb: 'HTML+ERB', 98 | rhtml: 'HTML+ERB', 99 | razor: 'HTML+Razor', 100 | hbs: 'Handlebars', 101 | htmlbars: 'Handlebars', 102 | hylang: 'Hy', 103 | igor: 'IGOR Pro', 104 | igorpro: 'IGOR Pro', 105 | dosini: 'INI', 106 | irc: 'IRC log', 107 | ignore: 'Ignore List', 108 | gitignore: 'Ignore List', 109 | ijm: 'ImageJ Macro', 110 | i7: 'Inform 7', 111 | inform7: 'Inform 7', 112 | geojson: 'JSON', 113 | jsonl: 'JSON', 114 | topojson: 'JSON', 115 | jsonc: 'JSON with Comments', 116 | jsp: 'Java Server Pages', 117 | js: 'JavaScript', 118 | node: 'JavaScript', 119 | mps: 'JetBrains MPS', 120 | django: 'Jinja', 121 | htmldjango: 'Jinja', 122 | ksy: 'Kaitai Struct', 123 | kak: 'KakouneScript', 124 | kakscript: 'KakouneScript', 125 | pcbnew: 'KiCad Layout', 126 | lassoscript: 'Lasso', 127 | flex: 'Lex', 128 | litcoffee: 'Literate CoffeeScript', 129 | lhaskell: 'Literate Haskell', 130 | lhs: 'Literate Haskell', 131 | ls: 'LiveScript', 132 | mumps: 'M', 133 | autoconf: 'M4Sugar', 134 | octave: 'MATLAB', 135 | m2: 'Macaulay2', 136 | bsdmake: 'Makefile', 137 | make: 'Makefile', 138 | mf: 'Makefile', 139 | pandoc: 'Markdown', 140 | markojs: 'Marko', 141 | mma: 'Mathematica', 142 | wolfram: 'Mathematica', 143 | wl: 'Mathematica', 144 | maxmsp: 'Max', 145 | m68k: 'Motorola 68K Assembly', 146 | amusewiki: 'Muse', 147 | npmrc: 'NPM Config', 148 | nixos: 'Nix', 149 | nush: 'Nu', 150 | njk: 'Nunjucks', 151 | objc: 'Objective-C', 152 | objectivec: 'Objective-C', 153 | objectivej: 'Objective-J', 154 | objj: 'Objective-J', 155 | odinlang: 'Odin', 156 | progress: 'OpenEdge ABL', 157 | openedge: 'OpenEdge ABL', 158 | abl: 'OpenEdge ABL', 159 | openrc: 'OpenRC runscript', 160 | AFDKO: 'OpenType Feature File', 161 | inc: 'PHP', 162 | povray: 'POV-Ray SDL', 163 | pasm: 'Parrot Assembly', 164 | pir: 'Parrot Internal Representation', 165 | delphi: 'Pascal', 166 | objectpascal: 'Pascal', 167 | cperl: 'Perl', 168 | postscr: 'PostScript', 169 | posh: 'PowerShell', 170 | pwsh: 'PowerShell', 171 | protobuf: 'Protocol Buffer', 172 | python3: 'Python', 173 | py: 'Python', 174 | rusthon: 'Python', 175 | pycon: 'Python console', 176 | qsharp: 'Q#', 177 | R: 'R', 178 | Rscript: 'R', 179 | splus: 'R', 180 | arexx: 'REXX', 181 | rpcgen: 'RPC', 182 | oncrpc: 'RPC', 183 | xdr: 'RPC', 184 | sqlrpgle: 'RPGLE', 185 | specfile: 'RPM Spec', 186 | perl6: 'Raku', 187 | raw: 'Raw token data', 188 | inputrc: 'Readline Config', 189 | readline: 'Readline Config', 190 | redirects: 'Redirect Rules', 191 | regexp: 'Regular Expression', 192 | regex: 'Regular Expression', 193 | renpy: "Ren'Py", 194 | groff: 'Roff', 195 | man: 'Roff', 196 | manpage: 'Roff', 197 | mdoc: 'Roff', 198 | nroff: 'Roff', 199 | troff: 'Roff', 200 | jruby: 'Ruby', 201 | macruby: 'Ruby', 202 | rake: 'Ruby', 203 | rb: 'Ruby', 204 | rbx: 'Ruby', 205 | rs: 'Rust', 206 | sepolicy: 'SELinux Policy', 207 | stla: 'STL', 208 | saltstate: 'SaltStack', 209 | salt: 'SaltStack', 210 | sh: 'Shell', 211 | bash: 'Shell', 212 | zsh: 'Shell', 213 | shellcheckrc: 'ShellCheck Config', 214 | console: 'ShellSession', 215 | coccinelle: 'SmPL', 216 | squeak: 'Smalltalk', 217 | sourcemod: 'SourcePawn', 218 | sml: 'Standard ML', 219 | bazel: 'Starlark', 220 | bzl: 'Starlark', 221 | latex: 'TeX', 222 | fundamental: 'Text', 223 | tl: 'Type Language', 224 | ts: 'TypeScript', 225 | gas: 'Unix Assembly', 226 | Ur: 'UrWeb', 227 | vlang: 'V', 228 | vb6: 'VBA', 229 | keyvalues: 'Valve Data Format', 230 | vdf: 'Valve Data Format', 231 | vtl: 'Velocity Template Language', 232 | velocity: 'Velocity Template Language', 233 | help: 'Vim Help File', 234 | vimhelp: 'Vim Help File', 235 | vim: 'Vim Script', 236 | viml: 'Vim Script', 237 | nvim: 'Vim Script', 238 | SnipMate: 'Vim Snippet', 239 | UltiSnip: 'Vim Snippet', 240 | UltiSnips: 'Vim Snippet', 241 | NeoSnippet: 'Vim Snippet', 242 | vbnet: 'Visual Basic .NET', 243 | wast: 'WebAssembly', 244 | wasm: 'WebAssembly', 245 | vtt: 'WebVTT', 246 | wgetrc: 'Wget Config', 247 | mediawiki: 'Wikitext', 248 | wiki: 'Wikitext', 249 | wrenlang: 'Wren', 250 | xbm: 'X BitMap', 251 | xpm: 'X PixMap', 252 | xten: 'X10', 253 | rss: 'XML', 254 | xsd: 'XML', 255 | wsdl: 'XML', 256 | xsl: 'XSLT', 257 | yml: 'YAML', 258 | snippet: 'YASnippet', 259 | yas: 'YASnippet', 260 | bro: 'Zeek', 261 | curlrc: 'cURL Config', 262 | rst: 'reStructuredText', 263 | robots: 'robots.txt', 264 | advpl: 'xBase', 265 | clipper: 'xBase', 266 | html: 'HTML', 267 | foxpro: 'xBase' 268 | }; 269 | if (dict[lang] !== undefined) { 270 | return dict[lang]; 271 | } 272 | return lang.charAt(0).toUpperCase() + lang.slice(1); 273 | }; 274 | -------------------------------------------------------------------------------- /tests/const/theme.test.ts: -------------------------------------------------------------------------------- 1 | import {ThemeMap} from '../../src/const/theme'; 2 | 3 | describe('Validate all theme', () => { 4 | it('theme colors are match the color regex', () => { 5 | // We validate short hex color, hex color and RGBA hex 6 | const colorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{8})$/; 7 | for (const theme of ThemeMap.values()) { 8 | expect(theme.title).toMatch(colorRegex); 9 | expect(theme.text).toMatch(colorRegex); 10 | expect(theme.background).toMatch(colorRegex); 11 | expect(theme.stroke).toMatch(colorRegex); 12 | expect(theme.icon).toMatch(colorRegex); 13 | expect(theme.chart).toMatch(colorRegex); 14 | } 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/github-api/commits-per-language.test.ts: -------------------------------------------------------------------------------- 1 | import {getCommitLanguage} from '../../src/github-api/commits-per-language'; 2 | import axios from 'axios'; 3 | import MockAdapter from 'axios-mock-adapter'; 4 | const mock = new MockAdapter(axios); 5 | 6 | const data = { 7 | data: { 8 | user: { 9 | contributionsCollection: { 10 | commitContributionsByRepository: [ 11 | { 12 | repository: { 13 | primaryLanguage: { 14 | name: 'Rust', 15 | color: '#dea584' 16 | } 17 | }, 18 | contributions: { 19 | totalCount: 99 20 | } 21 | }, 22 | { 23 | repository: { 24 | primaryLanguage: { 25 | name: 'JavaScript', 26 | color: '#f1e05a' 27 | } 28 | }, 29 | contributions: { 30 | totalCount: 84 31 | } 32 | }, 33 | { 34 | repository: { 35 | primaryLanguage: { 36 | name: 'Rust', 37 | color: '#dea584' 38 | } 39 | }, 40 | contributions: { 41 | totalCount: 100 42 | } 43 | }, 44 | { 45 | repository: { 46 | primaryLanguage: { 47 | name: 'Jupyter Notebook', 48 | color: '#f18e33' 49 | } 50 | }, 51 | contributions: { 52 | totalCount: 75 53 | } 54 | }, 55 | { 56 | repository: { 57 | primaryLanguage: null 58 | }, 59 | contributions: { 60 | totalCount: 100 61 | } 62 | } 63 | ] 64 | } 65 | } 66 | } 67 | }; 68 | 69 | const error = { 70 | errors: [ 71 | { 72 | type: 'NOT_FOUND', 73 | path: ['user'], 74 | locations: [], 75 | message: 'GitHub api failed' 76 | } 77 | ] 78 | }; 79 | 80 | afterEach(() => { 81 | mock.reset(); 82 | }); 83 | 84 | describe('commit contributions on github', () => { 85 | it('should get correct commit contributions', async () => { 86 | mock.onPost('https://api.github.com/graphql').reply(200, data); 87 | const totalContributions = await getCommitLanguage('vn7n24fzkq', []); 88 | expect(totalContributions).toEqual({ 89 | languageMap: new Map([ 90 | ['Rust', {color: '#dea584', count: 199, name: 'Rust'}], 91 | ['JavaScript', {color: '#f1e05a', count: 84, name: 'JavaScript'}], 92 | ['Jupyter Notebook', {color: '#f18e33', count: 75, name: 'Jupyter Notebook'}] 93 | ]) 94 | }); 95 | }); 96 | 97 | it('should throw error when api failed', async () => { 98 | mock.onPost('https://api.github.com/graphql').reply(200, error); 99 | await expect(getCommitLanguage('vn7n24fzkq', [])).rejects.toThrow('GitHub api failed'); 100 | }); 101 | 102 | it('should do a case-insensitive comparison for language exclusion', async () => { 103 | mock.onPost('https://api.github.com/graphql') 104 | .reply(200, data); 105 | const repoData = await getCommitLanguage('vn7n24fzkq', ['jupyter notebook']); 106 | expect(repoData).toEqual({ 107 | languageMap: new Map([ 108 | ['Rust', {color: '#dea584', count: 199, name: 'Rust'}], 109 | ['JavaScript', {color: '#f1e05a', count: 84, name: 'JavaScript'}] 110 | ]) 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /tests/github-api/contributions-by-year.test.ts: -------------------------------------------------------------------------------- 1 | import {getContributionByYear} from '../../src/github-api/contributions-by-year'; 2 | import axios from 'axios'; 3 | import MockAdapter from 'axios-mock-adapter'; 4 | const mock = new MockAdapter(axios); 5 | 6 | const data = { 7 | data: { 8 | user: { 9 | contributionsCollection: { 10 | totalCommitContributions: 30, 11 | contributionCalendar: { 12 | totalContributions: 10 13 | } 14 | } 15 | } 16 | } 17 | }; 18 | 19 | const error = { 20 | errors: [ 21 | { 22 | type: 'NOT_FOUND', 23 | path: ['user'], 24 | locations: [], 25 | message: 'GitHub api failed' 26 | } 27 | ] 28 | }; 29 | 30 | afterEach(() => { 31 | mock.reset(); 32 | }); 33 | 34 | describe('contributions count on github', () => { 35 | it('should get correct contributions', async () => { 36 | mock.onPost('https://api.github.com/graphql').reply(200, data); 37 | const totalContributions = await getContributionByYear('vn7n24fzkq', 2020); 38 | expect(totalContributions).toEqual({ 39 | totalCommitContributions: 30, 40 | totalContributions: 10, 41 | year: 2020 42 | }); 43 | }); 44 | 45 | it('should throw error when api failed', async () => { 46 | mock.onPost('https://api.github.com/graphql').reply(200, error); 47 | await expect(getContributionByYear('vn7n24fzkq', 2020)).rejects.toThrow('GitHub api failed'); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/github-api/profile-details.test.ts: -------------------------------------------------------------------------------- 1 | import {getProfileDetails} from '../../src/github-api/profile-details'; 2 | import axios from 'axios'; 3 | import MockAdapter from 'axios-mock-adapter'; 4 | const mock = new MockAdapter(axios); 5 | 6 | const data = { 7 | data: { 8 | user: { 9 | id: 'userID', 10 | name: 'vn7', 11 | email: 'vn7n24fzkq@gmail.com', 12 | createdAt: '2016-07-01T10:46:25Z', 13 | twitterUsername: null, 14 | company: 'vn7', 15 | location: 'Taiwan', 16 | websiteUrl: null, 17 | repositories: { 18 | totalCount: 30, 19 | nodes: [{stargazers: {totalCount: 110}}, {stargazers: {totalCount: 20}}] 20 | }, 21 | issues: {totalCount: 10}, 22 | repositoriesContributedTo: {totalCount: 30}, 23 | pullRequests: {totalCount: 40}, 24 | contributionsCollection: { 25 | contributionYears: [2019, 2020], 26 | contributionCalendar: { 27 | weeks: [ 28 | { 29 | contributionDays: [ 30 | { 31 | date: '2019-09-06T00:00:00.000+00:00', 32 | contributionCount: 20 33 | }, 34 | { 35 | date: '2019-09-07T00:00:00.000+00:00', 36 | contributionCount: 10 37 | } 38 | ] 39 | }, 40 | { 41 | contributionDays: [ 42 | { 43 | date: '2020-01-12T00:00:00.000+00:00', 44 | contributionCount: 5 45 | } 46 | ] 47 | } 48 | ] 49 | } 50 | } 51 | } 52 | } 53 | }; 54 | 55 | const error = { 56 | errors: [ 57 | { 58 | type: 'NOT_FOUND', 59 | path: ['user'], 60 | locations: [], 61 | message: 'GitHub api failed' 62 | } 63 | ] 64 | }; 65 | 66 | afterEach(() => { 67 | mock.reset(); 68 | }); 69 | 70 | describe('github api for profile details', () => { 71 | it('should get correct profile data', async () => { 72 | mock.onPost('https://api.github.com/graphql').reply(200, data); 73 | const profileDetails = await getProfileDetails('vn7n24fzkq'); 74 | expect(profileDetails).toEqual({ 75 | id: 'userID', 76 | name: 'vn7', 77 | email: 'vn7n24fzkq@gmail.com', 78 | createdAt: '2016-07-01T10:46:25Z', 79 | company: 'vn7', 80 | location: 'Taiwan', 81 | websiteUrl: null, 82 | twitterUsername: null, 83 | contributionYears: [2019, 2020], 84 | totalPublicRepos: 30, 85 | totalStars: 130, 86 | totalIssueContributions: 10, 87 | totalPullRequestContributions: 40, 88 | totalRepositoryContributions: 30, 89 | contributions: [ 90 | { 91 | date: new Date('2019-09-06T00:00:00.000+00:00'), 92 | contributionCount: 20 93 | }, 94 | { 95 | date: new Date('2019-09-07T00:00:00.000+00:00'), 96 | contributionCount: 10 97 | }, 98 | { 99 | date: new Date('2020-01-12T00:00:00.000+00:00'), 100 | contributionCount: 5 101 | } 102 | ] 103 | }); 104 | }); 105 | 106 | it('should throw error when api failed', async () => { 107 | mock.onPost('https://api.github.com/graphql').reply(200, error); 108 | await expect(getProfileDetails('vn7n24fzkq')).rejects.toThrow('GitHub api failed'); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /tests/github-api/repos-per-language.test.ts: -------------------------------------------------------------------------------- 1 | import {getRepoLanguages} from '../../src/github-api/repos-per-language'; 2 | import axios from 'axios'; 3 | import MockAdapter from 'axios-mock-adapter'; 4 | const mock = new MockAdapter(axios); 5 | 6 | const firstData = { 7 | data: { 8 | user: { 9 | repositories: { 10 | nodes: [ 11 | { 12 | primaryLanguage: { 13 | color: '#b07219', 14 | name: 'Java' 15 | } 16 | }, 17 | { 18 | primaryLanguage: { 19 | color: '#dea584', 20 | name: 'Rust' 21 | } 22 | } 23 | ], 24 | pageInfo: { 25 | endCursor: 'ABCD29yOnYyOpHOBslODA==', 26 | hasNextPage: true 27 | } 28 | } 29 | } 30 | } 31 | }; 32 | const lastData = { 33 | data: { 34 | user: { 35 | repositories: { 36 | nodes: [ 37 | { 38 | primaryLanguage: { 39 | color: '#b07219', 40 | name: 'Java' 41 | } 42 | }, 43 | { 44 | primaryLanguage: { 45 | color: '#f18e33', 46 | name: 'Kotlin' 47 | } 48 | } 49 | ], 50 | pageInfo: { 51 | endCursor: null, 52 | hasNextPage: false 53 | } 54 | } 55 | } 56 | } 57 | }; 58 | 59 | const error = { 60 | errors: [ 61 | { 62 | type: 'NOT_FOUND', 63 | path: ['user'], 64 | locations: [], 65 | message: 'GitHub api failed' 66 | } 67 | ] 68 | }; 69 | 70 | const dataContainingLanguageWithWhiteSpace = { 71 | data: { 72 | user: { 73 | repositories: { 74 | nodes: [ 75 | { 76 | primaryLanguage: { 77 | color: '#b07219', 78 | name: 'Rust' 79 | } 80 | }, 81 | { 82 | primaryLanguage: { 83 | color: '#f18e33', 84 | name: 'Kotlin' 85 | } 86 | }, 87 | { 88 | primaryLanguage: { 89 | color: '#f1948a', 90 | name: 'Jupyter Notebook' 91 | } 92 | }, 93 | { 94 | primaryLanguage: { 95 | color: '#f9e79f', 96 | name: 'Java' 97 | } 98 | } 99 | ], 100 | pageInfo: { 101 | endCursor: null, 102 | hasNextPage: false 103 | } 104 | } 105 | } 106 | } 107 | } 108 | 109 | afterEach(() => { 110 | mock.reset(); 111 | }); 112 | 113 | describe('repos per language on github', () => { 114 | it('should get correct data', async () => { 115 | mock.onPost('https://api.github.com/graphql') 116 | .replyOnce(200, firstData) 117 | .onPost('https://api.github.com/graphql') 118 | .replyOnce(200, lastData) 119 | .onAny(); 120 | const repoData = await getRepoLanguages('vn7n24fzkq', []); 121 | expect(repoData).toEqual({ 122 | languageMap: new Map([ 123 | ['Java', {color: '#b07219', count: 2, name: 'Java'}], 124 | ['Rust', {color: '#dea584', count: 1, name: 'Rust'}], 125 | ['Kotlin', {color: '#f18e33', count: 1, name: 'Kotlin'}] 126 | ]) 127 | }); 128 | }); 129 | 130 | it('should throw error when api failed', async () => { 131 | mock.onPost('https://api.github.com/graphql').reply(200, error); 132 | await expect(getRepoLanguages('vn7n24fzkq', [])).rejects.toThrow('GitHub api failed'); 133 | }); 134 | 135 | it('should do a case-insensitive comparison for language exclusion', async () => { 136 | mock.onPost('https://api.github.com/graphql') 137 | .reply(200, dataContainingLanguageWithWhiteSpace); 138 | const repoData = await getRepoLanguages('vn7n24fzkq', ['rust','jupyter notebook']); 139 | expect(repoData).toEqual({ 140 | languageMap: new Map([ 141 | ['Kotlin', {color: '#f18e33', count: 1, name: 'Kotlin'}], 142 | ['Java', {color: '#f9e79f', count: 1, name: 'Java'}] 143 | ]) 144 | }); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /tests/utils/file-writer.test.ts: -------------------------------------------------------------------------------- 1 | import {writeSVG, OUTPUT_PATH} from '../../src/utils/file-writer'; 2 | import {rmdirSync, readFileSync} from 'fs'; 3 | const targetFolder = `${OUTPUT_PATH}/test`; 4 | 5 | afterEach(() => { 6 | rmdirSync(targetFolder, {recursive: true}); 7 | }); 8 | describe('Test output function', () => { 9 | it('test write svg can work', () => { 10 | writeSVG('test', 'write-svg', 'work'); 11 | const content = readFileSync(`${targetFolder}/write-svg.svg`, { 12 | encoding: 'utf8', 13 | flag: 'r' 14 | }); 15 | expect(content).toEqual('work'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "outDir": "./lib", 6 | "rootDir": "./src", 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "esModuleInterop": true 10 | }, 11 | "exclude": ["node_modules", "**/*.test.ts", "tests/*.ts", "api/**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | }, 5 | "outputDirectory": "api/pages", 6 | "rewrites": [{"source": "/api/(.*)", "destination": "/api"}], 7 | "headers": [ 8 | { 9 | "source": "/api/cards/(.*)", 10 | "headers": [ 11 | { 12 | "key": "cache-control", 13 | "value": "public, max-age=21600, s-maxage=21600, stale-while-revalidate=14400" 14 | } 15 | ] 16 | }, 17 | { 18 | "source": "/api/theme", 19 | "headers": [ 20 | {"key": "Access-Control-Allow-Credentials", "value": "true"}, 21 | {"key": "Access-Control-Allow-Origin", "value": "*"}, 22 | { 23 | "key": "Access-Control-Allow-Methods", 24 | "value": "GET,OPTIONS,PATCH,DELETE,POST,PUT" 25 | }, 26 | { 27 | "key": "Access-Control-Allow-Headers", 28 | "value": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" 29 | } 30 | ] 31 | } 32 | ], 33 | "redirects": [ 34 | { 35 | "source": "/", 36 | "destination": "/demo" 37 | }, 38 | {"source": "/demo", "destination": "/demo.html"} 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /wiki/Home.md: -------------------------------------------------------------------------------- 1 | Welcome to the Github-profile-summary-cards wiki! 2 | 3 | 4 | 5 | 6 | Follow the tutorial [here](https://github.com/vn7n24fzkq/github-profile-summary-cards/wiki/Tutorial).
7 | Full resolution variants of gifs used in the tutorial are [here](https://github.com/vn7n24fzkq/github-profile-summary-cards/wiki/Videos).
8 | Explore the Legacy version [here](https://github.com/vn7n24fzkq/github-profile-summary-cards/wiki/Tutorial_legacy).
9 | -------------------------------------------------------------------------------- /wiki/Tutorial.md: -------------------------------------------------------------------------------- 1 | # Let's get started! 2 | This tutorial will help you deploy Github-profile-summary-cards with ease.
3 | If you have any issues regarding the gifs, you can find high res video verions [Here.](https://github.com/vn7n24fzkq/github-profile-summary-cards/wiki/Videos) 4 | 5 | ### Table of Contents 6 | 1. [Generate token](#generate-token) 7 | 2. [Select a repo](#select-a-repo) 8 | 9 | - [Create a new repo](#new-repo) 10 | - [Add to existing repo (Recommended ⭐)](#existing-repo) 11 | 3. [Create a workflow](#create-a-workflow) 12 | - [Additional information](#additional-information) 13 | 4. [Edit workflow file](#edit-workflow-file) 14 | 5. [Run the workflow](#run-the-workflow) 15 | 6. [Final Step](#final-step) 16 | 7. [The end](#the-end) 17 | 18 | 19 | # Generate token 20 | 21 | 1. Navigate to your profile's Settings -> Developer setting -> Personal access tokens -> Generate new token 22 | 23 | 24 | 25 | 2. Name your token 26 | (I'd recommend naming it something after `profile-summary-cards-token`) and ticking these boxes: 27 | 28 | 29 |

animated

30 | 31 | **Make sure these boxes are checked!** *not shown in the gif* 32 | 33 | ``` 34 | Repo 35 | - repo:status 36 | - repo_deployment 37 | - public_repo 38 | 39 | User 40 | - read:user 41 | - user:email 42 | ``` 43 | 44 | 45 | 46 | 3. And copy your token (**and don't lose it! You'd have to generate a new token**) 47 | 48 | 49 | 50 | # Select a repo 51 | 52 | - If you want to add to an already EXISTING repository. [Click here](#existing-repo) 53 | - (E.g. If you already have a README that shows up on your profile) 54 | - If you want to create a brand NEW repository. [Click here](#new-repo) 55 | 56 | 57 | ### New repo 58 | To Create a new repo from a template: 59 | 1. Go to [Template link](https://github.com/vn7n24fzkq/github-profile-summary-cards-example) 60 | 2. Click on the "Use this template" button in the top right corner 61 | 3. Select "Create a new template" 62 | 63 |

animated

64 | 65 | 4. Name the repo as your username
(E.g. `FunnyUsername/FunnyUsername`, this popup should appear if you've done it correctly) 66 |

animated

67 | 5. You can ignore the next sub-step. 68 | 69 | 70 | 71 | ### Existing repo 72 | --- 73 | 1. Add a README.md file **[if you dont have that file already]** 74 | 75 | 2. Rename your repo to your username (E.g. `FunnyUsername/FunnyUsername`) **[if you havent already]**
76 |

animated

77 | 3. That's pretty much all. lets continue 78 | 79 | 80 | # Create a workflow 81 | 82 | Now we will add a workflow to automatically update the summary cards. 83 | 84 | 1. Navigate to the repo's Actions -> New workflow -> Set up workflow yourself 85 | 2. Name your new workflow (I'd recommend naming it something after `profile-summary-cards`) 86 | * **Make sure you put `.yml` at the end!** 87 | 88 |

animated

89 | 90 | 4. **Commit changes!** 91 | 92 | Code snippet: 93 | ``` 94 | name: GitHub-Profile-Summary-Cards 95 | 96 | on: 97 | create: 98 | schedule: # execute every 24 hours 99 | - cron: "* */24 * * *" 100 | workflow_dispatch: 101 | 102 | jobs: 103 | build: 104 | runs-on: ubuntu-latest 105 | name: generate-github-cards 106 | permissions: 107 | contents: write 108 | 109 | steps: 110 | - uses: actions/checkout@v2 111 | - uses: vn7n24fzkq/github-profile-summary-cards@release 112 | env: 113 | GITHUB_TOKEN: ${{ secrets.[YOUR_SECRET_TOKEN_NAME] }} 114 | with: 115 | USERNAME: ${{ github.repository_owner }} 116 | ``` 117 | 118 | ## Additional information 119 | 120 | >Please note that the workflow in it's current configuration will run every 24h (it will update every 24h) if you want to change it here is a ``cron's job definition`` 121 | 122 | ```# .---------------- minute (0 - 59) 123 | # | .------------- hour (0 - 23) 124 | # | | .---------- day of month (1 - 31) 125 | # | | | .------- month (1 - 12) OR jan,feb,mar,apr ... 126 | # | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR 127 | # | | | | | sun,mon,tue,wed,thu,fri,sat 128 | # | | | | | 129 | # * * * * * 130 | ``` 131 | 132 | So let's say you want it to run every 15 hours:
133 | `- cron: "* */15 * * *"` 134 | 135 | Or for example you want it to run every friday at 12:35pm:
136 | `- cron: "35 12 * * fri"` 137 | 138 | # Create a Secret for Token 139 | 140 | 1. Navigate to repo's Settings -> Secrets and variables -> Actions -> Repository secrets -> New repository secret 141 | 142 | 2. Name your secret (again, I'd suggest naming your secret as `summary_card_token` or similar) 143 | 144 | 3. Past in your **Personal access token**. 145 | - in case you've lost it. Please go back to [Generate token](#generate-token) and get a new one. 146 | 147 | 4. **Copy that New secret's name!** 148 | 149 | 150 |

animated

151 | 152 | # Edit workflow file 153 | Now that we have obtained the Secret, we can move on the last step before deployment 🎉 154 | 155 | 1. Navigate back to Code -> .github -> workflows -> `profile-summary-cards.yml` *(or customized name you gave to the .yml file)* 156 | 157 | 2. Hit the pencil icon on the right side of your screen 158 | 159 | 3. Edit the `[YOUR_SECRET_TOKEN_NAME]` inside the `GITHUB_TOKEN: ${{ secrets.[YOUR_SECRET_TOKEN_NAME] }}` with the Secret 160 | 161 | - (Result should look something like this: `GITHUB_TOKEN: ${{ secrets.SUMMARY_CARD_TOKEN }}`) 162 | 163 | 4. Commit changes 164 | 165 | 166 |

animated

167 | 168 | 169 | # Run the workflow 170 | 171 | 1. Navigate to Actions -> on the left side `profile-summary-cards` -> hit the button `Run workflow` -> Run workflow 172 | 173 | 174 |

animated

175 | 176 | 2. Wait till the workflow run appears (if not, please refresh the site) 177 | 178 | - its normal that the loading indicator "gets stuck" at a certain point, just refresh the page. 179 | 180 | 3. If the loading indicators turn blue with a check inside, congratulations! 181 | - if it for some reason wont, you've probably messed up somewhere (or this tutorial got outdated!) I recommend going from the beginning again OR making a new repo and renaming it after you've successfully managed to deploy this app. 182 | 183 | # Final step 184 | You did it! now we are ready to choose the theme we want our cards to be in 185 | 1. Navigate to Code -> profile-summary-card-output -> Select a prefered theme -> README.md 186 | 187 | - You can get really creative with the layout you want your cards to be in, but for simplicity sake, I will pick a whole bundle 188 | 189 | 2. Copy the desired markdown section. 190 | 191 | 3. Navigate to Code -> Hit on pencil button on the right side of your README.md file 192 | 193 | 4. Paste in the copied content and hit Commit changes! 194 | 195 | 196 |

animated

197 | 198 | # The end 199 | 200 | Now if everything went right. The cards should appear on your profile! 201 | 202 | Don't be afraid to experiment with themes! There are many that might suit you better. 203 | 204 | --- 205 | That's it is guys. Thank you for going through this tutorial. I hope you found it somewhat helpful. 206 | 207 | if you find any typos or erros please open an issue so I can get on them asap. 208 | 209 | Have a pleasant rest of your day ^^ 210 | -------------------------------------------------------------------------------- /wiki/Tutorial_legacy.md: -------------------------------------------------------------------------------- 1 | ## IMPORTANT!
2 | 3 | This is LEGACY Version, New updated wiki can be found [Here.](https://github.com/vn7n24fzkq/github-profile-summary-cards/wiki/Tutorial) 4 | 5 | --- 6 | ### First step 7 | 8 | | We create a Personal access token with permissions we need | 9 | | :---------------------------------------------------------------------------: | 10 | | 1. Find `Settings` | 11 | | | 12 | | 2. Find `Developer Settings` | 13 | | | 14 | | 3. Find `Personal access tokens` | 15 | | | 16 | | 4. Press `Generate new token` button | 17 | | | 18 | | 5. Type access token name and check permissions | 19 | | | 20 | | 6. Scroll to bottom and press `Generate token` button | 21 | | > | 22 | | 7. Then we get the token, copy the token value, we will use it later | 23 | | ![](./assets_legacy/copy-token-value.png) | 24 | 25 | --- 26 | 27 | ### Add to repo 28 | 29 | - If you want create a Profile README or create a new repository. [Next Step](#use-template) 30 | 31 | - If you want add to a exist repository. [Next Step](#add-personal-access-token-to-repo) 32 | 33 | --- 34 | 35 | ### Use template 36 | 37 | | Open template page [github-profile-summary-cards-example](https://github.com/vn7n24fzkq/github-profile-summary-cards-example) | 38 | | :---------------------------------------------------------------------------------------------------------------------------------------------------------------: | 39 | | Find and press `Use this template` button | 40 | | | 41 | | Type repository name then press `Create repository from template` button (If you want to create a Profile README repository then the name should be you username) | 42 | | | 43 | | Now we have a new repository | 44 | | | 45 | 46 | [Next Step](#add-personal-access-token-to-repo) 47 | 48 | --- 49 | 50 | ### Add Github Action to repo 51 | 52 | | We are gonna use the personal token we early copy | 53 | | :--------------------------------------------------------------------: | 54 | | Find and click `Add file` button | 55 | | | 56 | | Type file name with path `.github/workflows/profile-summary-cards.yml` | 57 | | | 58 | | Copy and paste to the file | 59 | 60 | ```yml 61 | name: GitHub-Profile-Summary-Cards 62 | 63 | on: 64 | schedule: # execute every 24 hours 65 | - cron: "* */24 * * *" 66 | workflow_dispatch: 67 | 68 | jobs: 69 | build: 70 | runs-on: ubuntu-latest 71 | name: generate 72 | 73 | steps: 74 | - uses: actions/checkout@v2 75 | - uses: vn7n24fzkq/github-profile-summary-cards@release 76 | env: 77 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 78 | with: 79 | USERNAME: ${{ github.repository_owner }} 80 | ``` 81 | 82 | | It should looks like this one | 83 | | :-----------------------------------------------------------: | 84 | | | 85 | | Then we commit file | 86 | | | 87 | 88 | [Next Step](#add-personal-access-token-to-repo) 89 | 90 | --- 91 | 92 | ### Add Personal access token to repo 93 | 94 | | We are gonna use the personal token we early copy | 95 | | :-------------------------------------------------------------------------------------------------------------------: | 96 | | Find `Settings` in repository | 97 | | | 98 | | Find secrets in repository settings | 99 | | | 100 | | Now, we type secret name you want and paste the personal access token as secret Value, then press `Add secret` button | 101 | | | 102 | | It should has a secret here | 103 | | | 104 | 105 | [Next Step](#change-github-action-token) 106 | 107 | --- 108 | 109 | ### Change Github Action token 110 | 111 | | We are almost done! | 112 | | :------------------------------------------------------------------: | 113 | | Find the github action file just added | 114 | | | 115 | | And we do some modify this | 116 | | | 117 | | Replace default GITHUB_TOKEN with the secret we jsut add | 118 | | | 119 | | With new secret | 120 | | | 121 | | Commit this change | 122 | | | 123 | 124 | [Next Step](#trigger-action) 125 | 126 | ### Trigger action 127 | 128 | | Now the action should automatically start | 129 | | :--------------------------------------------------------------------: | 130 | | We can check workflow runs | 131 | | | 132 | | Run workflow manually | 133 | | | 134 | | Wait workflow finish (You need to refresh page to see latest workflow) | 135 | | | 136 | 137 | [Next Step](#everything-are-finished!) 138 | 139 | --- 140 | 141 | ### Everything are finished! 142 | 143 | | We can see all cards of each themes 🎉 | 144 | | :------------------------------------------------------: | 145 | | Check profile-summary-card-output folder in your repo | 146 | | | 147 | | :star: Finish :star: | 148 | | | 149 | -------------------------------------------------------------------------------- /wiki/Videos.md: -------------------------------------------------------------------------------- 1 | https://github.com/Cheezik/github-profile-summary-cards/assets/80171791/c93167c9-340e-4353-8950-d7b80a4b59a0 2 | 3 | 4 | https://github.com/Cheezik/github-profile-summary-cards/assets/80171791/d9805f94-10ee-4517-9a3f-ac50b63a78ec 5 | 6 | 7 | 8 | https://github.com/Cheezik/github-profile-summary-cards/assets/80171791/e61d6f36-7765-4e94-9de6-5473d7216a26 9 | 10 | 11 | 12 | https://github.com/Cheezik/github-profile-summary-cards/assets/80171791/3f41056a-6e12-4910-b18c-977b59a83501 13 | 14 | 15 | 16 | https://github.com/Cheezik/github-profile-summary-cards/assets/80171791/3e63ccd5-60ef-4636-a0a6-238371bf5db7 17 | 18 | 19 | 20 | https://github.com/Cheezik/github-profile-summary-cards/assets/80171791/8553ff1f-2ea6-4a0a-b87b-7ac029f1e04a 21 | 22 | 23 | 24 | https://github.com/Cheezik/github-profile-summary-cards/assets/80171791/61b5a2a9-78c9-4f4d-a547-f7a23e16c417 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /wiki/assets_legacy/commit-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/commit-file.png -------------------------------------------------------------------------------- /wiki/assets_legacy/commit-secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/commit-secret.png -------------------------------------------------------------------------------- /wiki/assets_legacy/copy-token-value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/copy-token-value.png -------------------------------------------------------------------------------- /wiki/assets_legacy/create-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/create-token.png -------------------------------------------------------------------------------- /wiki/assets_legacy/edit-workflow-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/edit-workflow-file.png -------------------------------------------------------------------------------- /wiki/assets_legacy/find-developer-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/find-developer-settings.png -------------------------------------------------------------------------------- /wiki/assets_legacy/find-personal-access-tokens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/find-personal-access-tokens.png -------------------------------------------------------------------------------- /wiki/assets_legacy/find-repo-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/find-repo-settings.png -------------------------------------------------------------------------------- /wiki/assets_legacy/find-secrets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/find-secrets.png -------------------------------------------------------------------------------- /wiki/assets_legacy/find-setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/find-setting.png -------------------------------------------------------------------------------- /wiki/assets_legacy/find-workflow-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/find-workflow-file.png -------------------------------------------------------------------------------- /wiki/assets_legacy/finish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/finish.png -------------------------------------------------------------------------------- /wiki/assets_legacy/generate-new-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/generate-new-token.png -------------------------------------------------------------------------------- /wiki/assets_legacy/generate-token-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/generate-token-button.png -------------------------------------------------------------------------------- /wiki/assets_legacy/new-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/new-file.png -------------------------------------------------------------------------------- /wiki/assets_legacy/new-repo-secret-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/new-repo-secret-button.png -------------------------------------------------------------------------------- /wiki/assets_legacy/new-repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/new-repo.png -------------------------------------------------------------------------------- /wiki/assets_legacy/new-secrect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/new-secrect.png -------------------------------------------------------------------------------- /wiki/assets_legacy/old-secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/old-secret.png -------------------------------------------------------------------------------- /wiki/assets_legacy/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/output.png -------------------------------------------------------------------------------- /wiki/assets_legacy/permission-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/permission-1.png -------------------------------------------------------------------------------- /wiki/assets_legacy/permission-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/permission-2.png -------------------------------------------------------------------------------- /wiki/assets_legacy/press-use-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/press-use-template.png -------------------------------------------------------------------------------- /wiki/assets_legacy/run-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/run-workflow.png -------------------------------------------------------------------------------- /wiki/assets_legacy/secret-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/secret-preview.png -------------------------------------------------------------------------------- /wiki/assets_legacy/type-file-name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/type-file-name.png -------------------------------------------------------------------------------- /wiki/assets_legacy/type-repo-name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/type-repo-name.png -------------------------------------------------------------------------------- /wiki/assets_legacy/type-token-and-token-value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/type-token-and-token-value.png -------------------------------------------------------------------------------- /wiki/assets_legacy/where-is-action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/where-is-action.png -------------------------------------------------------------------------------- /wiki/assets_legacy/where-is-add-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_legacy/where-is-add-file.png -------------------------------------------------------------------------------- /wiki/assets_new/Video_Full_Res/Step1.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_new/Video_Full_Res/Step1.mp4 -------------------------------------------------------------------------------- /wiki/assets_new/Video_Full_Res/Step2_Sub1.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_new/Video_Full_Res/Step2_Sub1.mp4 -------------------------------------------------------------------------------- /wiki/assets_new/Video_Full_Res/Step3.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_new/Video_Full_Res/Step3.mp4 -------------------------------------------------------------------------------- /wiki/assets_new/Video_Full_Res/Step4.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_new/Video_Full_Res/Step4.mp4 -------------------------------------------------------------------------------- /wiki/assets_new/Video_Full_Res/Step6.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_new/Video_Full_Res/Step6.mp4 -------------------------------------------------------------------------------- /wiki/assets_new/Video_Full_Res/Step7.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_new/Video_Full_Res/Step7.mp4 -------------------------------------------------------------------------------- /wiki/assets_new/Video_Full_Res/step5.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_new/Video_Full_Res/step5.mp4 -------------------------------------------------------------------------------- /wiki/assets_new/create_n_wrkflw.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_new/create_n_wrkflw.gif -------------------------------------------------------------------------------- /wiki/assets_new/edit_rdm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_new/edit_rdm.gif -------------------------------------------------------------------------------- /wiki/assets_new/edit_wrkflw.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_new/edit_wrkflw.gif -------------------------------------------------------------------------------- /wiki/assets_new/gen_user_pac.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_new/gen_user_pac.gif -------------------------------------------------------------------------------- /wiki/assets_new/make_n_scrt.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_new/make_n_scrt.gif -------------------------------------------------------------------------------- /wiki/assets_new/make_rep_f_tmp.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_new/make_rep_f_tmp.gif -------------------------------------------------------------------------------- /wiki/assets_new/run_wrkflw.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_new/run_wrkflw.gif -------------------------------------------------------------------------------- /wiki/assets_new/special_repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vn7n24fzkq/github-profile-summary-cards/c2fa12c268d76d3d84ff952be25c1b101968dba9/wiki/assets_new/special_repo.png --------------------------------------------------------------------------------