├── CNAME ├── .env.example ├── .npmrc ├── .npmrc.bak ├── .npmignore ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── bug_report.yml │ └── feature_request.yml ├── dependabot.yml ├── workflows │ ├── label-pr.yml │ ├── test.yml │ ├── prs-cache-clean.yml │ ├── publish.yml │ ├── top-issues-dashboard.yml │ ├── generate-theme-doc.yml │ ├── generate-locale-doc.yml │ ├── auto-build-pkg.yml │ └── codeql.yml ├── stale.yml ├── FUNDING.yml ├── labeler.yml ├── pull_request_template.md └── mergify.yml ├── coverage.yml ├── tsconfig.json ├── withexpress.ts ├── vercel.json ├── .eslintrc.json ├── tests ├── renderDocsReadme.test.ts ├── utils.test.ts ├── getToken.test.ts ├── renderStatsCard.test.ts └── card.test.ts ├── jest.config.json ├── src ├── common │ └── utils.ts ├── getToken.ts ├── fetcher │ ├── repositoryStats.ts │ └── stats.ts ├── getData.ts ├── icons.ts └── card.ts ├── LICENSE ├── SECURITY.md ├── package.json ├── .gitignore ├── scripts ├── generate-translation-doc.ts └── generate-theme-doc.ts ├── i18n ├── README.md ├── index.ts └── languageNames.ts ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── api └── index.ts ├── logo.svg ├── themes ├── README.md └── index.ts └── README.md /CNAME: -------------------------------------------------------------------------------- 1 | gh-readme-profile.vercel.app -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Replace ghp_xxx with your GitHub PAT token 2 | GH_TOKEN=ghp_xxx 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | //npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN} 2 | @fajarkim:registry=https://npm.pkg.github.com 3 | always-auth=true 4 | -------------------------------------------------------------------------------- /.npmrc.bak: -------------------------------------------------------------------------------- 1 | //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN} 2 | @fajarkim:registry=https://registry.npmjs.org 3 | always-auth=true 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage.yml 2 | jest.config.json 3 | .npmrc 4 | .npmrc.bak 5 | 6 | .github/ 7 | tests/ 8 | scripts/ 9 | node_modules/ 10 | coverage/ 11 | public/ 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Question 4 | url: https://github.com/FajarKim/github-readme-profile/discussions 5 | about: Please ask and answer questions here. -------------------------------------------------------------------------------- /coverage.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: "70...100" 8 | 9 | status: 10 | project: 11 | default: 12 | threshold: 5 13 | patch: false 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "outDir": "./public", 6 | "rootDir": "./", 7 | "strict": true, 8 | "moduleResolution": "node", 9 | "esModuleInterop": true 10 | }, 11 | "exclude": ["node_modules"] 12 | } 13 | -------------------------------------------------------------------------------- /withexpress.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import dotenv from "dotenv"; 3 | import readmeStats from "./api/index"; 4 | 5 | dotenv.config(); 6 | const app = express(); 7 | 8 | app.use("/", readmeStats); 9 | 10 | const port: number = Number(process.env.PORT) || 3000; 11 | 12 | app.listen(port, () => { 13 | console.log(`Running on http://localhost:${port}`); 14 | }); 15 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "api/*.ts": { 4 | "memory": 128, 5 | "maxDuration": 10 6 | } 7 | }, 8 | "redirects": [ 9 | { 10 | "source": "/", 11 | "destination": "https://gh-readme-profile-generator.vercel.app/" 12 | } 13 | ], 14 | "headers": [ 15 | { 16 | "source": "/api", 17 | "headers": [ 18 | { 19 | "key": "Cache-Control", 20 | "value": "s-maxage=7200, stale-while-revalidate" 21 | } 22 | ] 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for NPM 4 | - package-ecosystem: npm 5 | directory: "/" 6 | schedule: 7 | interval: weekly 8 | open-pull-requests-limit: 10 9 | commit-message: 10 | prefix: "build(deps)" 11 | prefix-development: "build(deps-dev)" 12 | 13 | # Maintain dependencies for GitHub Actions 14 | - package-ecosystem: github-actions 15 | directory: "/" 16 | schedule: 17 | interval: weekly 18 | open-pull-requests-limit: 10 19 | commit-message: 20 | prefix: "build(deps)" 21 | prefix-development: "build(deps-dev)" 22 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "jest": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaVersion": 12, 16 | "sourceType": "module" 17 | }, 18 | "plugins": [ 19 | "@typescript-eslint" 20 | ], 21 | "rules": { 22 | "@typescript-eslint/explicit-module-boundary-types": "on", 23 | "@typescript-eslint/no-explicit-any": "error" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/label-pr.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Labeler" 2 | on: 3 | - pull_request_target 4 | 5 | permissions: 6 | actions: read 7 | checks: read 8 | contents: read 9 | deployments: read 10 | issues: read 11 | discussions: read 12 | packages: read 13 | pages: read 14 | pull-requests: write 15 | repository-projects: read 16 | security-events: read 17 | statuses: read 18 | 19 | jobs: 20 | triage: 21 | if: github.repository == 'FajarKim/github-readme-profile' 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 25 | with: 26 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 27 | sync-labels: true 28 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 30 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - feature 8 | - enhancement 9 | - help wanted 10 | - bug 11 | 12 | # Label to use when marking an issue as stale 13 | staleLabel: stale 14 | # Comment to post when marking an issue as stale. Set to `false` to disable 15 | markComment: > 16 | This issue has been automatically marked as stale because it has not had 17 | recent activity. It will be closed if no further activity occurs. Thank you 18 | for your contributions. 19 | # Comment to post when closing a stale issue. Set to `false` to disable 20 | closeComment: false 21 | -------------------------------------------------------------------------------- /tests/renderDocsReadme.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { generateReadmeThemes } from "../scripts/generate-theme-doc"; 3 | import { generateReadmeLocales } from "../scripts/generate-translation-doc"; 4 | 5 | jest.mock("fs"); 6 | 7 | describe("Test Generate Readme Docs", () => { 8 | it("should generate the README content themes correctly", () => { 9 | const username = "FajarKim"; 10 | const generatedReadme = generateReadmeThemes(username); 11 | 12 | expect(fs.writeFileSync).toHaveBeenCalledWith("./themes/README.md", generatedReadme); 13 | }); 14 | 15 | it("should generate the README content locales correctly", () => { 16 | const generatedReadme = generateReadmeLocales(); 17 | 18 | expect(fs.writeFileSync).toHaveBeenCalledWith("./i18n/README.md", generatedReadme); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ FajarKim ] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: fajarkim # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: [ 13 | "https://trakteer.id/fajarkim", 14 | ] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | permissions: read-all 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 18 | 19 | - name: Setup Node 20 | uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 21 | with: 22 | node-version: "22.x" 23 | cache: npm 24 | 25 | - name: Install, Build, and Test Package 26 | run: | 27 | npm install 28 | npm install typescript 29 | npm run build 30 | npm run test 31 | env: 32 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | - name: Code Coverage 35 | uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 36 | env: 37 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 38 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest", 3 | "testEnvironment": "node", 4 | "testMatch": [ 5 | "/tests/**/*.test.ts" 6 | ], 7 | "transform": { 8 | "^.+\\.tsx?$": [ 9 | "ts-jest", 10 | { 11 | "tsconfig": "tsconfig.json", 12 | "diagnotics": true 13 | } 14 | ] 15 | }, 16 | "moduleFileExtensions": [ 17 | "ts", 18 | "tsx", 19 | "js", 20 | "jsx", 21 | "json", 22 | "node" 23 | ], 24 | "testPathIgnorePatterns": [ 25 | "/node_modules/", 26 | "/public/" 27 | ], 28 | "collectCoverage": true, 29 | "coverageDirectory": "coverage", 30 | "collectCoverageFrom": [ 31 | "/api/**/*.ts", 32 | "/scripts/**/*.ts", 33 | "/src/**/*.ts", 34 | "/themes/**/*.ts", 35 | "/i18n/**/*.ts" 36 | ], 37 | "coverageReporters": [ 38 | "lcov", 39 | "text", 40 | "clover", 41 | "html" 42 | ], 43 | "coveragePathIgnorePatterns": [ 44 | "/node_modules/", 45 | "/public/" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /tests/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { isValidHexColor, isValidGradient } from "../src/common/utils"; 2 | 3 | describe("isValidHexColor function", () => { 4 | it("should return true for valid hexadecimal colors", () => { 5 | expect(isValidHexColor("ff0000")).toBe(true); 6 | expect(isValidHexColor("00ff00")).toBe(true); 7 | expect(isValidHexColor("0000ff")).toBe(true); 8 | expect(isValidHexColor("123abc")).toBe(true); 9 | expect(isValidHexColor("abcdef12")).toBe(true); 10 | }); 11 | 12 | it("should return false for invalid hexadecimal colors", () => { 13 | expect(isValidHexColor("12345")).toBe(false); 14 | expect(isValidHexColor("abcdefg")).toBe(false); 15 | expect(isValidHexColor("blue")).toBe(false); 16 | }); 17 | }); 18 | 19 | describe("isValidGradient function", () => { 20 | it("should return true for valid gradients", () => { 21 | expect(isValidGradient("45,ff0000,00ff00")).toBe(true); 22 | }); 23 | 24 | it("should return false for invalid gradients", () => { 25 | expect(isValidGradient("45,invalid,green")).toBe(false); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/common/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks whether the provided value is a valid hexadecimal color. 3 | * 4 | * @param {any} hexColor - The input value to check for valid hexadecimal color. 5 | * @returns {boolean} - True if the input is a valid hexadecimal color, false otherwise. 6 | */ 7 | function isValidHexColor(hexColor: string): boolean { 8 | const re = new RegExp("^([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "g"); 9 | return re.test(hexColor); 10 | } 11 | 12 | /** 13 | * Checks whether the provided array of hexadecimal colors is a valid gradient. 14 | * 15 | * @param {string[]} hexColors - The array of hexadecimal colors to check for a valid gradient. 16 | * @returns {boolean} - True if the array represents a valid gradient, false otherwise. 17 | */ 18 | function isValidGradient(hexColors: string): boolean { 19 | const colors = hexColors.split(","); 20 | for (let i = 1; i < colors.length; i++) { 21 | if (!isValidHexColor(colors[i])) { 22 | return false; 23 | } 24 | } 25 | return true; 26 | } 27 | 28 | export { 29 | isValidHexColor, 30 | isValidGradient 31 | } 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a report to help us improve. 3 | labels: 4 | - "bug" 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | :information_source: You can join my community and report bugs in the dedicated group. [Join now](https://chat.whatsapp.com/DM6WaaTQ4IJ3VOe2ly6KJD)! 10 | - type: textarea 11 | attributes: 12 | label: Describe the bug 13 | description: A clear and concise description of what the bug is. 14 | validations: 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: Expected behavior 19 | description: 20 | A clear and concise description of what you expected to happen. 21 | - type: textarea 22 | attributes: 23 | label: Screenshots / Live demo link 24 | description: If applicable, add screenshots to help explain your problem. 25 | placeholder: Paste the github-readme-profile link as markdown image 26 | - type: textarea 27 | attributes: 28 | label: Additional context 29 | description: Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Rangga Fajar Oktariansyah and Contributors 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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project. 3 | labels: 4 | - "enhancement" 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | :information_source: You can join my community and request features in the dedicated group. [Join now](https://chat.whatsapp.com/DM6WaaTQ4IJ3VOe2ly6KJD)! 10 | - type: textarea 11 | attributes: 12 | label: Is your feature request related to a problem? Please describe. 13 | description: 14 | A clear and concise description of what the problem is. Ex. I'm always 15 | frustrated when [...] 16 | validations: 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: Describe the solution you'd like 21 | description: A clear and concise description of what you want to happen. 22 | - type: textarea 23 | attributes: 24 | label: Describe alternatives you've considered 25 | description: 26 | A clear and concise description of any alternative solutions or features 27 | you've considered. 28 | - type: textarea 29 | attributes: 30 | label: Additional context 31 | description: 32 | Add any other context or screenshots about the feature request here. 33 | -------------------------------------------------------------------------------- /src/getToken.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import { getInput } from "@actions/core"; 3 | 4 | dotenv.config(); 5 | 6 | /** 7 | * Retrieves the GitHub token from the environment variables or GitHub Actions inputs. 8 | * 9 | * @param {boolean} bearerHeader - Flag indicating whether to return the token with 'Bearer' prefix. 10 | * @returns {string} - The GitHub token. 11 | */ 12 | function getToken(bearerHeader: boolean): string { 13 | const getEnvirontment: any = process.env; 14 | let getGHEnvirontment: any = Object.keys(getEnvirontment).filter((key) => 15 | key.startsWith("GH_TOKEN") 16 | ); 17 | getGHEnvirontment = getGHEnvirontment.map((key: string) => getEnvirontment[key]); 18 | 19 | // Select a random GitHub environment variable 20 | let getGHToken: string = 21 | getGHEnvirontment[Math.floor(Math.random() * getGHEnvirontment.length)]; 22 | 23 | // If no GitHub environment variable is found, get the token from GitHub Actions inputs 24 | if (!getGHToken) { 25 | getGHToken = getInput("github_token"); 26 | 27 | if (!getGHToken) { 28 | throw new Error("Could not find github token"); 29 | } 30 | } 31 | 32 | if (bearerHeader) { 33 | return `Bearer ${getGHToken}`; 34 | } 35 | 36 | return getGHToken; 37 | } 38 | 39 | export default getToken; 40 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | card: 2 | - changed-files: 3 | - any-glob-to-any-file: 4 | - "src/card.ts" 5 | 6 | card-i18n: 7 | - changed-files: 8 | - any-glob-to-any-file: 9 | - "src/translations.ts" 10 | 11 | ci: 12 | - changed-files: 13 | - any-glob-to-any-file: 14 | - ".github/workflows/*" 15 | - "scripts/*" 16 | 17 | dependencies: 18 | - changed-files: 19 | - any-glob-to-any-file: 20 | - "package.json" 21 | - "package-lock.json" 22 | 23 | doc-translation: 24 | - changed-files: 25 | - any-glob-to-any-file: 26 | - "i18n/README.md" 27 | 28 | doc-theme: 29 | - changed-files: 30 | - any-glob-to-any-file: 31 | - "themes/README.md" 32 | 33 | documentation: 34 | - changed-files: 35 | - any-glob-to-any-file: 36 | - "README.md" 37 | - "CONTRIBUTING.md" 38 | - "CODE_OF_CONDUCT.md" 39 | - "SECURITY.md" 40 | 41 | github_actions: 42 | - changed-files: 43 | - any-glob-to-any-file: 44 | - ".github/*.yml" 45 | - ".github/workflows/*" 46 | 47 | stats-card: 48 | - changed-files: 49 | - any-glob-to-any-file: 50 | - "api/index.ts" 51 | - "src/fetcher/*" 52 | 53 | themes: 54 | - changed-files: 55 | - any-glob-to-any-file: 56 | - "themes/index.ts" 57 | -------------------------------------------------------------------------------- /.github/workflows/prs-cache-clean.yml: -------------------------------------------------------------------------------- 1 | name: prs cache clean 2 | on: 3 | pull_request: 4 | types: 5 | - closed 6 | 7 | permissions: 8 | actions: write 9 | checks: read 10 | contents: read 11 | deployments: read 12 | issues: read 13 | discussions: read 14 | packages: read 15 | pages: read 16 | pull-requests: read 17 | repository-projects: read 18 | security-events: read 19 | statuses: read 20 | 21 | jobs: 22 | cleanup: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 27 | 28 | - name: Cleanup 29 | run: | 30 | gh extension install actions/gh-actions-cache 31 | 32 | REPO=${{ github.repository }} 33 | BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge" 34 | 35 | echo "Fetching list of cache key" 36 | cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 ) 37 | 38 | ## Setting this to not fail the workflow while deleting cache keys. 39 | set +e 40 | echo "Deleting caches..." 41 | for cacheKey in $cacheKeysForPR 42 | do 43 | gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm 44 | done 45 | echo "Done" 46 | env: 47 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | 5 | Fixes # 6 | 7 | ### Type of change 8 | 9 | 10 | 11 | - [ ] Bug fix (added a non-breaking change which fixes an issue) 12 | - [ ] New feature (added a non-breaking change which adds functionality) 13 | - [ ] Updated documentation (updated the readme, templates, or other repo files) 14 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 15 | - [ ] Misc change (updated other files non-breaking change) 16 | 17 | ## How Has This Been Tested? 18 | 19 | 23 | 24 | - [ ] Tested locally with a valid username 25 | - [ ] Tested locally with an invalid username 26 | - [ ] Ran tests with `npm test` 27 | - [ ] Added or updated test cases to test new features 28 | 29 | ## Checklist: 30 | 31 | - [ ] I have checked to make sure no other [pull requests](https://github.com/FajarKim/github-readme-profile/pulls?q=is%3Apr+sort%3Aupdated-desc+) are open for this issue 32 | - [ ] The code is properly formatted and is consistent with the existing code style 33 | - [ ] I have commented my code, particularly in hard-to-understand areas 34 | - [ ] I have made corresponding changes to the documentation 35 | - [ ] My changes generate no new warnings 36 | 37 | ## Screenshots 38 | 39 | 40 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to GitHub Packages and NPM.js 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | github-packages: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: read 11 | packages: write 12 | steps: 13 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 14 | # Setup .npmrc file to publish to GitHub Packages 15 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 16 | with: 17 | node-version: "22.x" 18 | registry-url: "https://npm.pkg.github.com" 19 | # Defaults to the user or organization that owns the workflow file 20 | scope: "@fajarkim" 21 | - run: npm ci 22 | - run: npm publish --public 23 | env: 24 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | 26 | npm: 27 | runs-on: ubuntu-latest 28 | permissions: 29 | contents: read 30 | packages: write 31 | steps: 32 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 33 | # Setup .npmrc file to publish to NPM.js 34 | - run: mv .npmrc.bak .npmrc 35 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 36 | with: 37 | node-version: "22.x" 38 | registry-url: "https://registry.npmjs.org" 39 | # Defaults to the user or organization that owns the workflow file 40 | scope: "@fajarkim" 41 | - run: sed -i "s@https://npm.pkg.github.com@https://registry.npmjs.org@g" package.json 42 | - run: npm ci 43 | - run: npm publish --public 44 | env: 45 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 46 | -------------------------------------------------------------------------------- /.github/mergify.yml: -------------------------------------------------------------------------------- 1 | queue_rules: 2 | - name: dep-update 3 | batch_size: 100 4 | batch_max_wait_time: 30 min 5 | queue_conditions: 6 | - author=dependabot[bot] 7 | 8 | pull_request_rules: 9 | - name: Automatic approve for Dependabot pull requests 10 | conditions: 11 | - author=dependabot[bot] 12 | actions: 13 | review: 14 | type: APPROVE 15 | 16 | - name: Automatic merge for Dependabot pull requests 17 | conditions: 18 | - author=dependabot[bot] 19 | - "#approved-reviews-by>=1" 20 | - check-success=test 21 | actions: 22 | queue: 23 | 24 | - name: Automatic approve for GitHub Actions Bot pull requests 25 | conditions: 26 | - author=github-actions[bot] 27 | actions: 28 | review: 29 | type: APPROVE 30 | 31 | - name: Automatic approve for GitHub Readme Profile pull requests 32 | conditions: 33 | - author=gh-readme-profile 34 | actions: 35 | review: 36 | type: APPROVE 37 | 38 | - name: Automatic merge for GitHub Readme Profile pull requests with label "doc-theme" 39 | conditions: 40 | - author=gh-readme-profile 41 | - "#approved-reviews-by>=1" 42 | - label=doc-theme 43 | actions: 44 | merge: 45 | 46 | - name: Automatic merge for GitHub Readme Profile pull requests with label "doc-translation" 47 | conditions: 48 | - author=gh-readme-profile 49 | - "#approved-reviews-by>=1" 50 | - label=doc-translation 51 | actions: 52 | merge: 53 | 54 | - name: Automatic merge for GitHub Readme Profile pull requests with label "dependencies" 55 | conditions: 56 | - author=gh-readme-profile 57 | - "#approved-reviews-by>=1" 58 | - label=dependencies 59 | - check-success=test 60 | actions: 61 | merge: 62 | -------------------------------------------------------------------------------- /.github/workflows/top-issues-dashboard.yml: -------------------------------------------------------------------------------- 1 | name: Update top issues dashboard 2 | on: 3 | schedule: 4 | # ┌───────────── minute (0 - 59) 5 | # │ ┌───────────── hour (0 - 23) 6 | # │ │ ┌───────────── day of the month (1 - 31) 7 | # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) 8 | # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) 9 | # │ │ │ │ │ 10 | # │ │ │ │ │ 11 | # │ │ │ │ │ 12 | # * * * * * 13 | - cron: "0 0 */10 * *" 14 | workflow_dispatch: 15 | 16 | permissions: 17 | actions: read 18 | checks: read 19 | contents: read 20 | deployments: read 21 | issues: write 22 | discussions: read 23 | packages: read 24 | pages: read 25 | pull-requests: write 26 | repository-projects: read 27 | security-events: read 28 | statuses: read 29 | 30 | jobs: 31 | show-and-label-top-issues: 32 | if: github.repository == 'FajarKim/github-readme-profile' 33 | name: Update top issues Dashboard. 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Run top issues action 37 | uses: rickstaa/top-issues-action@7e8dda5d5ae3087670f9094b9724a9a091fc3ba1 # v1.3.101 38 | env: 39 | github_token: ${{ secrets.GITHUB_TOKEN }} 40 | with: 41 | top_list_size: 10 42 | label: true 43 | dashboard: true 44 | dashboard_show_total_reactions: true 45 | top_issues: true 46 | top_bugs: true 47 | top_features: true 48 | top_pull_requests: true 49 | custom_pull_requests_label: themes 50 | top_custom_pull_requests_label: ":star: top themes" 51 | top_custom_pull_requests_label_description: Top themes 52 | top_custom_pull_requests_label_colour: "#A23599" 53 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # GitHub Readme Profile Security Policies and Procedures 2 | 3 | This document outlines security procedures and general policies for the 4 | GitHub Readme Profile project. 5 | 6 | - [Reporting a Vulnerability](#reporting-a-vulnerability) 7 | - [Disclosure Policy](#disclosure-policy) 8 | 9 | ## Reporting a Vulnerability 10 | 11 | The GitHub Readme Profile team and community take all security vulnerabilities 12 | seriously. Thank you for improving the security of our open source 13 | software. We appreciate your efforts and responsible disclosure and will 14 | make every effort to acknowledge your contributions. 15 | 16 | Report security vulnerabilities by emailing the GitHub Readme Profile team at: 17 | 18 | [![Mail](https://custom-icon-badges.demolab.com/badge/Mail-fajarrkim%40gmail.com-blue?s&labelColor=302d41&color=b7bdf8&logoColor=d9e0ee&style=for-the-badge&logo=mail)](mailto:fajarrkim@gmail.com) 19 | 20 | The lead maintainer will acknowledge your email within 24 hours, and will 21 | send a more detailed response within 48 hours indicating the next steps in 22 | handling your report. After the initial reply to your report, the security 23 | team will endeavor to keep you informed of the progress towards a fix and 24 | full announcement, and may ask for additional information or guidance. 25 | 26 | Report security vulnerabilities in third-party modules to the person or 27 | team maintaining the module. 28 | 29 | ## Disclosure Policy 30 | 31 | When the security team receives a security bug report, they will assign it 32 | to a primary handler. This person will coordinate the fix and release 33 | process, involving the following steps: 34 | 35 | * Confirm the problem. 36 | * Audit code to find any potential similar problems. 37 | * Prepare fixes and release them as fast as possible. 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fajarkim/github-readme-profile", 3 | "version": "1.1.2", 4 | "description": "🙀 Generate your Stats GitHub Profile in SVG", 5 | "main": "api/index.ts", 6 | "dependencies": { 7 | "@actions/core": "^2.0.1", 8 | "@barudakrosul/parse-boolean": "^0.0.2", 9 | "@resvg/resvg-js": "^2.6.2", 10 | "@types/escape-html": "^1.0.4", 11 | "@types/express": "^5.0.6", 12 | "@types/node": "^25.0.2", 13 | "axios": "^1.13.2", 14 | "dotenv": "^17.2.3", 15 | "escape-html": "^1.0.3", 16 | "express": "^5.2.1", 17 | "millify": "^6.1.0", 18 | "node-base64-image": "^2.2.0", 19 | "sharp": "^0.34.5" 20 | }, 21 | "scripts": { 22 | "test": "jest", 23 | "dev": "nodemon ./withexpress.ts", 24 | "build": "tsc", 25 | "theme-readme-gen": "ts-node scripts/generate-theme-doc.ts", 26 | "locale-readme-gen": "ts-node scripts/generate-translation-doc.ts", 27 | "start": "node ./public/withexpress.js" 28 | }, 29 | "author": "Rangga Fajar Oktariansyah", 30 | "license": "MIT", 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/FajarKim/github-readme-profile.git" 34 | }, 35 | "publishConfig": { 36 | "registry": "https://npm.pkg.github.com", 37 | "access": "public" 38 | }, 39 | "keywords": [ 40 | "github-readme-stats", 41 | "github-stats", 42 | "github-readme-profile", 43 | "github-profile-stats" 44 | ], 45 | "bugs": { 46 | "url": "https://github.com/FajarKim/github-readme-profile/issues" 47 | }, 48 | "homepage": "https://github.com/FajarKim/github-readme-profile#readme", 49 | "funding": [ 50 | { 51 | "type": "individual", 52 | "url": "https://buymeacoffee.com/fajarkim" 53 | } 54 | ], 55 | "engines": { 56 | "node": ">=20" 57 | }, 58 | "devDependencies": { 59 | "@markdoc/markdoc": "^0.5.4", 60 | "@types/jest": "^29.5.14", 61 | "@typescript-eslint/eslint-plugin": "^8.50.0", 62 | "@typescript-eslint/parser": "^8.50.0", 63 | "eslint": "^9.39.2", 64 | "jest": "^29.7.0", 65 | "nodemon": "^3.1.11", 66 | "ts-jest": "^29.4.6", 67 | "ts-node": "^10.9.2", 68 | "typescript": "^5.9.3" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.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 | # TypeScript v1 declaration files 45 | typings/ 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 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | public 107 | public/* 108 | .vercel 109 | coverage 110 | -------------------------------------------------------------------------------- /scripts/generate-translation-doc.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import locales from "../i18n/index"; 3 | import languageNames from "../i18n/languageNames"; 4 | 5 | const TARGET_FILE = "./i18n/README.md"; 6 | 7 | function getProgressColor(progress: number): string { 8 | if (progress <= 20) return "FF0000"; 9 | if (progress <= 40) return "FF7F00"; 10 | if (progress <= 60) return "FFFF00"; 11 | if (progress <= 80) return "7FFF00"; 12 | return "00FF00"; 13 | } 14 | 15 | function generateTranslationsMarkdown(locale: string): string { 16 | return `${locale}`; 17 | } 18 | 19 | export function generateReadmeLocales() { 20 | const availableLocales = Object.keys(locales).sort(); 21 | 22 | let localesListTable = ""; 23 | for (let i = 0; i < availableLocales.length; i += 1) { 24 | const localesSlice = availableLocales.slice(i, i + 1); 25 | const row = localesSlice.map(locale => generateTranslationsMarkdown(locale)).join(""); 26 | 27 | const progress = (Object.keys(locales[row]).length / 16) * 100; 28 | const progressColor = getProgressColor(progress); 29 | 30 | localesListTable += ` 31 |

${row}

32 |

${languageNames[row]}

33 |

${progress.toFixed(0)}%

34 | \n`; 35 | } 36 | 37 | const readmeContent = ` 38 | ## Available Locales 39 | Use \`?locale=LOCALE_CODE\` parameter like so :- 40 | 41 | \`\`\`markdown 42 | ![GitHub Stats](https://gh-readme-profile.vercel.app/api?username=FajarKim&locale=id) 43 | \`\`\` 44 | 45 | ## Locales List 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ${localesListTable}

Code

Locale

Progress

54 | 55 | Want to add new translations? Consider reading the [contribution guidelines](https://github.com/FajarKim/github-readme-profile/blob/master/CONTRIBUTING.md#%EF%B8%8F-translations-contribution) :D 56 | `; 57 | 58 | return readmeContent; 59 | } 60 | 61 | const generatedReadme = generateReadmeLocales(); 62 | 63 | fs.writeFileSync(TARGET_FILE, generatedReadme); 64 | -------------------------------------------------------------------------------- /scripts/generate-theme-doc.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import themes from "../themes/index"; 3 | 4 | const TARGET_FILE = "./themes/README.md"; 5 | 6 | function generateThemeMarkdown(theme: string): string { 7 | return `\`${theme}\`
![${theme}][${theme}]`; 8 | } 9 | 10 | function generateThemeLink(username: string, theme: string): string { 11 | return `[${theme}]: https://github-readme-profile-alpha.vercel.app/api?username=${username}&theme=${theme}`; 12 | } 13 | 14 | function createThemeRows( 15 | themesArray: string[], 16 | itemsPerRow: number, 17 | username: string 18 | ): { themesPreviewTable: string; themesPreviewLink: string } { 19 | let themesPreviewTable = ""; 20 | let themesPreviewLink = ""; 21 | 22 | for (let i = 0; i < themesArray.length; i += itemsPerRow) { 23 | const rowThemes = themesArray.slice(i, i + itemsPerRow); 24 | 25 | themesPreviewTable += `| ${rowThemes.map(generateThemeMarkdown).join(" | ")} |\n`; 26 | themesPreviewLink += rowThemes.map(theme => generateThemeLink(username, theme)).join("\n") + "\n"; 27 | } 28 | 29 | return { themesPreviewTable, themesPreviewLink }; 30 | } 31 | 32 | export function generateReadmeThemes(username: string): string { 33 | const availableThemes = Object.keys(themes); 34 | const itemsPerRow = 3; 35 | 36 | const { themesPreviewTable, themesPreviewLink } = createThemeRows(availableThemes, itemsPerRow, username); 37 | 38 | const readmeContent = ` 39 | ## Available Themes 40 | 41 | With inbuilt themes, you can customize the look of the card without doing any manual customization. 42 | 43 | Use \`?theme=THEME_NAME\` parameter like so :- 44 | 45 | \`\`\`md 46 | ![GitHub Profile](https://gh-readme-profile.vercel.app/api?username=${username}&theme=dark) 47 | \`\`\` 48 | 49 | ## Themes Preview 50 | 51 | | | | | 52 | | :---------------: | :---------------: | :---------------: | 53 | ${themesPreviewTable} 54 | 55 | Want to add a new theme? Consider reading the [contribution guidelines](/CONTRIBUTING.md#-themes-contribution) :D 56 | 57 | ${themesPreviewLink}`; 58 | 59 | return readmeContent; 60 | } 61 | 62 | const username = "FajarKim"; 63 | 64 | const generatedReadme = generateReadmeThemes(username); 65 | 66 | fs.writeFileSync(TARGET_FILE, generatedReadme); 67 | -------------------------------------------------------------------------------- /.github/workflows/generate-theme-doc.yml: -------------------------------------------------------------------------------- 1 | name: Generate Theme Readme 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - "themes/index.ts" 9 | - "scripts/generate-theme-doc.ts" 10 | 11 | permissions: 12 | actions: read 13 | checks: read 14 | contents: write 15 | deployments: read 16 | issues: read 17 | discussions: read 18 | packages: read 19 | pages: read 20 | pull-requests: write 21 | repository-projects: read 22 | security-events: read 23 | statuses: read 24 | 25 | jobs: 26 | generate-theme-doc: 27 | runs-on: ubuntu-latest 28 | 29 | steps: 30 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 31 | 32 | - name: Setup Node 33 | uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 34 | with: 35 | node-version: "22.x" 36 | cache: npm 37 | 38 | # Fix the unsafe repo error which was introduced by the CVE-2022-24765 git patches. 39 | - name: Fix unsafe repo error 40 | run: git config --global --add safe.directory ${{ github.workspace }} 41 | 42 | - name: Run npm install and generate readme 43 | run: | 44 | npm ci 45 | npm run theme-readme-gen 46 | env: 47 | CI: true 48 | 49 | - name: Set up Git 50 | run: | 51 | git config user.name "gh-readme-profile" 52 | git config user.email "githubreadmeprofile@gmail.com" 53 | git config --global --add safe.directory ${GITHUB_WORKSPACE} 54 | 55 | - name: Push commit to a new branch and create prs 56 | run: | 57 | branch="auto_update_theme_readme" 58 | message="docs(theme): auto update theme readme" 59 | body="_This pull request was created automatically._" 60 | if [[ "$(git status --porcelain)" != "" ]]; then 61 | git branch -D ${branch} || true 62 | git checkout -b ${branch} 63 | git add themes/README.md 64 | git commit --message "${message}" 65 | git remote add origin-${branch} "https://github.com/FajarKim/github-readme-profile.git" 66 | git push --force --quiet --set-upstream origin-${branch} ${branch} 67 | gh pr create --title "${message}" --body "${body}" 68 | fi 69 | env: 70 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 71 | -------------------------------------------------------------------------------- /.github/workflows/generate-locale-doc.yml: -------------------------------------------------------------------------------- 1 | name: Generate Translation Readme 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - "i18n/index.ts" 9 | - "scripts/generate-translation-doc.ts" 10 | 11 | permissions: 12 | actions: read 13 | checks: read 14 | contents: write 15 | deployments: read 16 | issues: read 17 | discussions: read 18 | packages: read 19 | pages: read 20 | pull-requests: write 21 | repository-projects: read 22 | security-events: read 23 | statuses: read 24 | 25 | jobs: 26 | generate-locale-doc: 27 | runs-on: ubuntu-latest 28 | 29 | steps: 30 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 31 | 32 | - name: Setup Node 33 | uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 34 | with: 35 | node-version: "22.x" 36 | cache: npm 37 | 38 | # Fix the unsafe repo error which was introduced by the CVE-2022-24765 git patches. 39 | - name: Fix unsafe repo error 40 | run: git config --global --add safe.directory ${{ github.workspace }} 41 | 42 | - name: Run npm install and generate readme 43 | run: | 44 | npm ci 45 | npm run locale-readme-gen 46 | env: 47 | CI: true 48 | 49 | - name: Set up Git 50 | run: | 51 | git config user.name "gh-readme-profile" 52 | git config user.email "githubreadmeprofile@gmail.com" 53 | git config --global --add safe.directory ${GITHUB_WORKSPACE} 54 | 55 | - name: Push commit to a new branch and create prs 56 | run: | 57 | branch="auto_update_locale_readme" 58 | message="docs(i18n): auto update translation readme" 59 | body="_This pull request was created automatically._" 60 | if [[ "$(git status --porcelain)" != "" ]]; then 61 | git branch -D ${branch} || true 62 | git checkout -b ${branch} 63 | git add i18n/README.md 64 | git commit --message "${message}" 65 | git remote add origin-${branch} "https://github.com/FajarKim/github-readme-profile.git" 66 | git push --force --quiet --set-upstream origin-${branch} ${branch} 67 | gh pr create --title "${message}" --body "${body}" 68 | fi 69 | env: 70 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 71 | -------------------------------------------------------------------------------- /.github/workflows/auto-build-pkg.yml: -------------------------------------------------------------------------------- 1 | name: Auto Build All Dependencies and DevDependencies 2 | on: 3 | schedule: 4 | # ┌───────────── minute (0 - 59) 5 | # │ ┌───────────── hour (0 - 23) 6 | # │ │ ┌───────────── day of the month (1 - 31) 7 | # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) 8 | # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) 9 | # │ │ │ │ │ 10 | # │ │ │ │ │ 11 | # │ │ │ │ │ 12 | # * * * * * 13 | - cron: "0 0 */7 * *" 14 | workflow_dispatch: 15 | 16 | permissions: 17 | actions: read 18 | checks: read 19 | contents: write 20 | deployments: read 21 | issues: read 22 | discussions: read 23 | packages: read 24 | pages: read 25 | pull-requests: write 26 | repository-projects: read 27 | security-events: read 28 | statuses: read 29 | 30 | jobs: 31 | auto-build: 32 | runs-on: ubuntu-latest 33 | 34 | steps: 35 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 36 | 37 | - name: Setup Node 38 | uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 39 | with: 40 | node-version: "22.x" 41 | cache: npm 42 | 43 | # Fix the unsafe repo error which was introduced by the CVE-2022-24765 git patches. 44 | - name: Fix unsafe repo error 45 | run: git config --global --add safe.directory ${{ github.workspace }} 46 | 47 | - name: Update git repository 48 | run: git pull 49 | 50 | - name: Run npm install and update 51 | run: | 52 | npm ci 53 | npm update --save 54 | npm audit fix 55 | env: 56 | CI: true 57 | 58 | - name: Set up Git 59 | run: | 60 | git config user.name "gh-readme-profile" 61 | git config user.email "githubreadmeprofile@gmail.com" 62 | git config --global --add safe.directory ${GITHUB_WORKSPACE} 63 | 64 | - name: Push commit to a new branch 65 | run: | 66 | branch="auto_build_and_bump" 67 | message="build(deps): bump all dependencies and devDependencies version" 68 | body="_This pull request was created automatically._" 69 | if [[ "$(git status --porcelain)" != "" ]]; then 70 | git branch -D ${branch} || true 71 | git checkout -b ${branch} 72 | git add package.json package-lock.json 73 | git commit --message "${message}" 74 | git remote add origin-${branch} "https://github.com/FajarKim/github-readme-profile.git" 75 | git push --force --quiet --set-upstream origin-${branch} ${branch} 76 | gh pr create --title "${message}" --body "${body}" 77 | fi 78 | env: 79 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 80 | -------------------------------------------------------------------------------- /src/fetcher/repositoryStats.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import getToken from "../getToken"; 3 | 4 | /** 5 | * Type representing the data associated with a user's repository stats. 6 | * 7 | * @typedef {Object} RepositoryData 8 | * @property {number} stars - The total count of stars across repositories. 9 | * @property {number} forks - The total count of forks across repositories. 10 | * @property {number} openedIssues - The total count of opened issues across repositories. 11 | */ 12 | type RepositoryData = { 13 | stars: number; 14 | forks: number; 15 | openedIssues: number; 16 | }; 17 | 18 | /** 19 | * Retrieves and calculates repository statistics for a given user. 20 | * 21 | * @param {string} username - The username of the GitHub user. 22 | * @param {number} totalpage - The total number of pages to retrieve data from. 23 | * @returns {Promise} - A promise that resolves to the repository statistics. 24 | */ 25 | async function repositoryStats( 26 | username: string, 27 | totalpage: number 28 | ): Promise { 29 | let stars = 0; 30 | let forks = 0; 31 | let openedIssues = 0; 32 | 33 | await Promise.all( 34 | Array.from( 35 | { length: totalpage }, 36 | async (_, i) => await getPerPageRepositoryData(username, i + 1) 37 | ) 38 | ).then((data: object[]) => { 39 | data.forEach((repo: any) => { 40 | stars += repo.stars; 41 | forks += repo.forks; 42 | openedIssues += repo.openedIssues; 43 | }); 44 | }); 45 | 46 | return { 47 | stars, 48 | forks, 49 | openedIssues, 50 | }; 51 | } 52 | 53 | /** 54 | * Retrieves repository data for a specific page. 55 | * 56 | * @param {string} username - The username of the GitHub user. 57 | * @param {number} pageno - The page number to retrieve data from. 58 | * @returns {Promise} - A promise that resolves to the repository data for the specified page. 59 | */ 60 | async function getPerPageRepositoryData( 61 | username: string, 62 | pageno: number 63 | ): Promise { 64 | const sanitizedUsername = encodeURIComponent(username); 65 | 66 | const data = await axios({ 67 | method: "get", 68 | url: `https://api.github.com/users/${sanitizedUsername}/repos?page=${pageno}&per_page=100`, 69 | headers: { 70 | "User-Agent": "FajarKim/github-readme-profile", 71 | Authorization: getToken(true), 72 | }, 73 | }); 74 | 75 | let stars = 0; 76 | let forks = 0; 77 | let openedIssues = 0; 78 | 79 | data.data.forEach((repo: any) => { 80 | stars += repo.stargazers_count; 81 | forks += repo.forks_count; 82 | openedIssues += repo.open_issues; 83 | }); 84 | 85 | return { 86 | stars, 87 | forks, 88 | openedIssues, 89 | }; 90 | } 91 | 92 | export { 93 | RepositoryData, 94 | repositoryStats, 95 | getPerPageRepositoryData 96 | }; 97 | export default repositoryStats; 98 | -------------------------------------------------------------------------------- /tests/getToken.test.ts: -------------------------------------------------------------------------------- 1 | import getToken from "../src/getToken"; 2 | import * as core from "@actions/core"; 3 | 4 | jest.mock("dotenv"); 5 | jest.mock("@actions/core", () => ({ 6 | getInput: jest.fn(), 7 | setSecret: jest.fn() // Mock setSecret to avoid masking 8 | })); 9 | 10 | type MockedGetInput = jest.MockedFunction; 11 | 12 | const mockedGetInput = core.getInput as MockedGetInput; 13 | 14 | describe("Test getToken function", () => { 15 | const originalEnv = process.env; 16 | 17 | beforeEach(() => { 18 | process.env = { ...originalEnv }; 19 | mockedGetInput.mockClear(); 20 | }); 21 | 22 | afterAll(() => { 23 | process.env = originalEnv; 24 | }); 25 | 26 | it("should return a personal token from GH_TOKEN without Bearer prefix", () => { 27 | process.env.GH_TOKEN = "ghp_token"; 28 | mockedGetInput.mockReturnValue(""); // Simulate no input from core.getInput 29 | 30 | const token = getToken(false); 31 | 32 | // Token should not be masked 33 | expect(token).toEqual("ghp_token"); 34 | }); 35 | 36 | it("should return a personal token from GH_TOKEN with Bearer prefix", () => { 37 | process.env.GH_TOKEN = "ghp_token"; 38 | mockedGetInput.mockReturnValue(""); // Simulate no input from core.getInput 39 | 40 | const token = getToken(true); 41 | 42 | // Token should not be masked 43 | expect(token).toEqual("Bearer ghp_token"); 44 | }); 45 | 46 | it("should return a GitHub Actions bot token from getInput without Bearer prefix when GH_TOKEN is absent", () => { 47 | delete process.env.GH_TOKEN; // Ensure GH_TOKEN is undefined 48 | mockedGetInput.mockReturnValue("GitHubActionsBotToken"); 49 | 50 | const token = getToken(false); 51 | 52 | // Check both the real token and the masked value 53 | expect(token === "GitHubActionsBotToken" || token === "***").toBeTruthy(); 54 | }); 55 | 56 | it("should return a GitHub Actions bot token from getInput with Bearer prefix when GH_TOKEN is absent", () => { 57 | delete process.env.GH_TOKEN; // Ensure GH_TOKEN is undefined 58 | mockedGetInput.mockReturnValue("GitHubActionsBotToken"); 59 | 60 | const token = getToken(true); 61 | 62 | // Check both the real token and the masked value 63 | expect(token === "Bearer GitHubActionsBotToken" || token === "Bearer ***").toBeTruthy(); 64 | }); 65 | 66 | it("should prioritize GH_TOKEN over getInput", () => { 67 | process.env.GH_TOKEN = "ghp_token"; // GH_TOKEN is present 68 | mockedGetInput.mockReturnValue("GitHubActionsBotToken"); 69 | 70 | const token = getToken(false); 71 | 72 | // GH_TOKEN should take priority 73 | expect(token).toEqual("ghp_token"); 74 | }); 75 | 76 | it("should throw an error if neither GH_TOKEN nor getInput tokens are available", () => { 77 | delete process.env.GH_TOKEN; // Ensure GH_TOKEN is undefined 78 | mockedGetInput.mockReturnValue(""); // No token from core.getInput 79 | 80 | expect(() => getToken(false)).toThrowError("Could not find github token"); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /i18n/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Available Locales 3 | Use `?locale=LOCALE_CODE` parameter like so :- 4 | 5 | ```markdown 6 | ![GitHub Stats](https://gh-readme-profile.vercel.app/api?username=FajarKim&locale=id) 7 | ``` 8 | 9 | ## Locales List 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |

Code

Locale

Progress

ar

Arabic

100%

en

English

100%

es

Spanish

100%

fa

Persian

100%

fr

French

100%

id

Indonesian

100%

ja

Japanese

100%

ko

Korean

100%

ms

Malay

100%

pt

Portuguese

100%

pt-BR

Portuguese (Brazil)

100%

su

Sundanese

100%

78 | 79 | Want to add new translations? Consider reading the [contribution guidelines](https://github.com/FajarKim/github-readme-profile/blob/master/CONTRIBUTING.md#%EF%B8%8F-translations-contribution) :D 80 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '15 5 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | # Runner size impacts CodeQL analysis time. To learn more, please see: 27 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 28 | # - https://gh.io/supported-runners-and-hardware-resources 29 | # - https://gh.io/using-larger-runners 30 | # Consider using larger runners for possible analysis time improvements. 31 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 32 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 33 | permissions: 34 | actions: read 35 | contents: read 36 | security-events: write 37 | 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | language: [ 'javascript-typescript' ] 42 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] 43 | # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both 44 | # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 45 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 46 | 47 | steps: 48 | - name: Checkout repository 49 | uses: actions/checkout@v6.0.1 50 | 51 | # Initializes the CodeQL tools for scanning. 52 | - name: Initialize CodeQL 53 | uses: github/codeql-action/init@v4 54 | with: 55 | languages: ${{ matrix.language }} 56 | # If you wish to specify custom queries, you can do so here or in a config file. 57 | # By default, queries listed here will override any specified in a config file. 58 | # Prefix the list here with "+" to use these queries and those in the config file. 59 | 60 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 61 | # queries: security-extended,security-and-quality 62 | 63 | 64 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). 65 | # If this step fails, then you should remove it and run the build manually (see below) 66 | - name: Autobuild 67 | uses: github/codeql-action/autobuild@v4 68 | 69 | # ℹ️ Command-line programs to run using the OS shell. 70 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 71 | 72 | # If the Autobuild fails above, remove it and uncomment the following three lines. 73 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 74 | 75 | # - run: | 76 | # echo "Run, Build Application using script" 77 | # ./location_of_script_within_repo/buildscript.sh 78 | 79 | - name: Perform CodeQL Analysis 80 | uses: github/codeql-action/analyze@v4 81 | with: 82 | category: "/language:${{matrix.language}}" 83 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [github-readme-profile](https://github.com/FajarKim/github-readme-profile) 2 | 3 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 4 | 5 | - Reporting [an issue](https://github.com/FajarKim/github-readme-profile/issues/new?assignees=&labels=bug&template=bug_report.yml). 6 | - [Discussing](https://github.com/FajarKim/github-readme-profile/discussions) the current state of the code. 7 | - Submitting [a fix](https://github.com/FajarKim/github-readme-profile/compare). 8 | - Proposing [new features](https://github.com/FajarKim/github-readme-profile/issues/new?assignees=&labels=enhancement&template=feature_request.yml). 9 | - Becoming a maintainer. 10 | 11 | ## 🌝 All Changes Happen Through Pull Requests 12 | 13 | Pull requests are the best way to propose changes. We actively welcome your pull requests: 14 | 15 | 1. Fork the repo and create your branch from `master`. 16 | 2. If you've added code that should be tested, add some tests' examples. 17 | 3. If you've changed APIs, update the documentation. 18 | 4. Issue that pull request! 19 | 20 | ## 🎨 Themes Contribution 21 | 22 | GitHub Readme Profile supports custom theming, and you can also contribute new themes! 23 | 24 | > [!NOTE]\ 25 | > If you are contributing your theme just because you are using it personally, then you can [customize the looks](./README.md#customization) of your card with URL params instead. 26 | 27 | > [!NOTE]\ 28 | > Your pull request with theme addition will be merged once we get enough positive feedback from the community in the form of thumbs up 👍 emojis. We expect to see at least 5-10 thumbs up before making a decision to merge your pull request into the master branch. Remember that you can also support themes of other contributors that you liked to speed up their merge. 29 | 30 | > [!NOTE]\ 31 | > Before submitting pull request, please make sure that your theme pass WCAG 2.0 level AA contrast ration test. You can use [this tool](https://webaim.org/resources/contrastchecker/) to check it. 32 | 33 | To contribute your theme you need to edit the [themes/index.ts](/themes/index.ts) file and add it at the end of the file. 34 | 35 | ## 🗣️ Translations Contribution 36 | GitHub Readme Profile supports multiple languages, if we are missing your language, you can contribute it! You can check the currently supported languages [here](/README.md#available-locales). 37 | 38 | To contribute your language you need to edit the [i18n/index.ts](/i18n/index.ts) file and add new property to each object where the key is the language code in ISO 639-1 standard and the value is the translated string. Anything appearing in [the list](https://gist.github.com/FajarKim/91516c2aecbfc8bf65f584d528d5f2b1) should be fine. 39 | 40 | ## 📑 Any contributions you make will be under the MIT Software License 41 | 42 | In short, when you submit changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. 43 | 44 | ## ⚠️ Report issues/bugs using GitHub's [issues](https://github.com/FajarKim/github-readme-profile/issues) 45 | 46 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/FajarKim/github-readme-profile/issues/new/choose); it's that easy! 47 | 48 | ### 🚨 Bug Reports 49 | 50 | **Great Bug Reports** tend to have: 51 | 52 | - A quick summary and/or background 53 | - Steps to reproduce 54 | - Be specific! 55 | - Share the snapshot, if possible. 56 | - GitHub Readme Stats' live link 57 | - What actually happens 58 | - What you expected would happen 59 | - Notes (possibly including why you think this might be happening or stuff you tried that didn't work) 60 | 61 | People ❤️ thorough bug reports. I'm not even kidding. 62 | 63 | 64 | ### 😎 Feature Request 65 | 66 | **Great Feature Requests** tend to have: 67 | 68 | - A quick idea summary 69 | - What & why do you want to add the specific feature 70 | - Additional context like images, links to resources to implement the feature, etc. 71 | 72 | ## 📖 License 73 | 74 | By contributing, you agree that your contributions will be licensed under its [MIT License](./LICENSE). 75 | -------------------------------------------------------------------------------- /src/getData.ts: -------------------------------------------------------------------------------- 1 | import millify from "millify"; 2 | import stats from "./fetcher/stats"; 3 | import repositoryStats from "./fetcher/repositoryStats"; 4 | const base64ImageFetcher = require("node-base64-image"); 5 | 6 | /** 7 | * Type representing GitHub user information. 8 | * 9 | * @typedef {Object} GetData 10 | * @property {string} username - GitHub username. 11 | * @property {string} name - GitHub user's name. 12 | * @property {string|Buffer} picture - Base64-encoded image or Buffer representing the user's profile picture. 13 | * @property {string|number} public_repos - Formatted count of public repositories. 14 | * @property {string|number} followers - Formatted count of followers. 15 | * @property {string|number} following - Formatted count of users being followed. 16 | * @property {string|number} total_stars - Formatted count of total stars received on repositories. 17 | * @property {string|number} total_forks - Formatted count of total forks received on repositories. 18 | * @property {string|number} total_issues - Formatted count of total issues (both open and closed). 19 | * @property {string|number} total_closed_issues - Formatted count of closed issues. 20 | * @property {string|number} total_prs - Formatted count of total pull requests. 21 | * @property {string|number} total_prs_merged - Formatted count of total merged pull requests. 22 | * @property {string|number} total_commits - Formatted count of total commits. 23 | * @property {string|number} total_review - Formatted count of pull request reviews. 24 | * @property {string|number} total_discussion_answered - Formatted count of discussions answered. 25 | * @property {string|number} total_discussion_started - Formatted count of discussions started. 26 | * @property {string|number} total_contributed_to - Formatted count of repositories contributed to. 27 | */ 28 | type GetData = { 29 | username: string; 30 | name: string; 31 | picture: string | Buffer; 32 | public_repos: string | number; 33 | followers: string | number; 34 | following: string | number; 35 | total_stars: string | number; 36 | total_forks: string | number; 37 | total_issues: string | number; 38 | total_closed_issues: string | number; 39 | total_prs: string | number; 40 | total_prs_merged: string | number; 41 | total_commits: string | number; 42 | total_review: string | number; 43 | total_discussion_answered: string | number; 44 | total_discussion_started: string | number; 45 | total_contributed_to: string | number; 46 | }; 47 | 48 | /** 49 | * Fetches and formats GitHub user data based on the provided username. 50 | * 51 | * @param {string} username - GitHub username. 52 | * @returns {Promise} - A promise that resolves with formatted GitHub user data. 53 | */ 54 | async function getData(username: string): Promise { 55 | const user = await stats(username); 56 | const totalRepoPages = Math.ceil(user.repositories.totalCount / 100); 57 | const userRepositories = await repositoryStats(username, totalRepoPages); 58 | 59 | if (!user.name) user.name = user.login; 60 | 61 | const output = { 62 | username: user.login, 63 | name: user.name, 64 | picture: await base64ImageFetcher.encode(`${user.avatarUrl}&s=200`, { 65 | string: true 66 | }), 67 | public_repos: millify(user.repositories.totalCount), 68 | followers: millify(user.followers.totalCount), 69 | following: millify(user.following.totalCount), 70 | total_stars: millify(userRepositories.stars), 71 | total_forks: millify(userRepositories.forks), 72 | total_issues: millify( 73 | user.openedIssues.totalCount + user.closedIssues.totalCount 74 | ), 75 | total_closed_issues: millify(user.closedIssues.totalCount), 76 | total_prs: millify(user.pullRequests.totalCount), 77 | total_prs_merged: millify(user.mergedPullRequests.totalCount), 78 | total_commits: millify( 79 | user.restrictedContributionsCount + user.totalCommitContributions 80 | ), 81 | total_review: millify(user.totalPullRequestReviewContributions), 82 | total_discussion_answered: millify(user.discussionAnswered.totalCount), 83 | total_discussion_started: millify(user.discussionStarted.totalCount), 84 | total_contributed_to: millify(user.repositoriesContributedTo.totalCount), 85 | }; 86 | 87 | return output; 88 | } 89 | 90 | export { GetData, getData }; 91 | export default getData; 92 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [INSERT CONTACT METHOD]. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations -------------------------------------------------------------------------------- /tests/renderStatsCard.test.ts: -------------------------------------------------------------------------------- 1 | import readmeStats from "../api/index"; 2 | import getData from "../src/getData"; 3 | import type { User } from "../src/fetcher/stats"; 4 | import card from "../src/card"; 5 | import themes from "../themes/index"; 6 | import locales from "../i18n/index"; 7 | 8 | jest.mock("../src/getData"); 9 | jest.mock("../src/card"); 10 | 11 | jest.mock("express", () => ({ 12 | ...jest.requireActual("express"), 13 | __esModule: true, 14 | default: { 15 | Request: jest.fn(), 16 | Response: jest.fn(), 17 | }, 18 | })); 19 | 20 | const exampleUserData: User = { 21 | name: "Fajar", 22 | login: "FajarKim", 23 | avatarUrl: "base64-encoded-image", 24 | repositories: { totalCount: 20 }, 25 | followers: { totalCount: 20 }, 26 | following: { totalCount: 10 }, 27 | openedIssues: { totalCount: 150 }, 28 | closedIssues: { totalCount: 100 }, 29 | pullRequests: { totalCount: 530 }, 30 | mergedPullRequests: { totalCount: 490 }, 31 | discussionStarted: { totalCount: 4 }, 32 | discussionAnswered: { totalCount: 10 }, 33 | repositoriesContributedTo: { totalCount: 12 }, 34 | totalCommitContributions: 1200, 35 | restrictedContributionsCount: 130, 36 | totalPullRequestReviewContributions: 340, 37 | }; 38 | 39 | describe("Test GitHub Readme Profile API", () => { 40 | let mockRequest: any; 41 | let mockResponse: any; 42 | 43 | beforeEach(() => { 44 | mockRequest = { 45 | query: {}, 46 | }; 47 | 48 | mockResponse = { 49 | json: jest.fn(), 50 | send: jest.fn(), 51 | setHeader: jest.fn(), 52 | status: jest.fn(), 53 | }; 54 | 55 | jest.clearAllMocks(); 56 | }); 57 | 58 | it("should handle request and generate JSON response", async () => { 59 | mockRequest.query.username = "FajarKim"; 60 | mockRequest.query.format = "json"; 61 | (getData as jest.Mock).mockResolvedValueOnce(exampleUserData); 62 | 63 | await readmeStats(mockRequest, mockResponse); 64 | 65 | expect(getData).toHaveBeenCalledWith(mockRequest.query.username); 66 | expect(mockResponse.json).toHaveBeenCalledWith(exampleUserData); 67 | expect(mockResponse.send).not.toHaveBeenCalled(); 68 | expect(mockResponse.setHeader).toHaveBeenCalledWith("Cache-Control", "s-maxage=7200, stale-while-revalidate"); 69 | }); 70 | 71 | it("should handle request and generate SVG response", async () => { 72 | mockRequest.query.username = "FajarKim"; 73 | mockRequest.query.format = "svg"; 74 | (getData as jest.Mock).mockResolvedValueOnce(exampleUserData); 75 | 76 | await readmeStats(mockRequest, mockResponse); 77 | 78 | expect(getData).toHaveBeenCalledWith(mockRequest.query.username); 79 | expect(card).toHaveBeenCalledWith(exampleUserData, expect.any(Object)); 80 | expect(mockResponse.send).toHaveBeenCalled(); 81 | expect(mockResponse.json).not.toHaveBeenCalled(); 82 | expect(mockResponse.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); 83 | }); 84 | 85 | it.each(Object.keys(themes))("should handle request theme '%s' and generate SVG response", async (theme) => { 86 | mockRequest.query.username = "FajarKim"; 87 | mockRequest.query.theme = theme; 88 | (getData as jest.Mock).mockResolvedValueOnce(exampleUserData); 89 | 90 | await readmeStats(mockRequest, mockResponse); 91 | 92 | expect(getData).toHaveBeenCalledWith(mockRequest.query.username); 93 | expect(card).toHaveBeenCalledWith(exampleUserData, expect.any(Object)); 94 | expect(mockResponse.send).toHaveBeenCalled(); 95 | expect(mockResponse.json).not.toHaveBeenCalled(); 96 | expect(mockResponse.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); 97 | }); 98 | 99 | it.each(Object.keys(locales))("should handle request locale '%s' and generate SVG response", async (locale) => { 100 | mockRequest.query.username = "FajarKim"; 101 | mockRequest.query.locale = locale; 102 | (getData as jest.Mock).mockResolvedValueOnce(exampleUserData); 103 | 104 | await readmeStats(mockRequest, mockResponse); 105 | 106 | expect(getData).toHaveBeenCalledWith(mockRequest.query.username); 107 | expect(card).toHaveBeenCalledWith(exampleUserData, expect.any(Object)); 108 | expect(mockResponse.send).toHaveBeenCalled(); 109 | expect(mockResponse.json).not.toHaveBeenCalled(); 110 | expect(mockResponse.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); 111 | }); 112 | 113 | it("should handle request hide stats and generate SVG response", async () => { 114 | mockRequest.query.username = "FajarKim"; 115 | mockRequest.query.hide = "repos,forks,prs_merged"; 116 | (getData as jest.Mock).mockResolvedValueOnce(exampleUserData); 117 | 118 | await readmeStats(mockRequest, mockResponse); 119 | 120 | expect(getData).toHaveBeenCalledWith(mockRequest.query.username); 121 | expect(card).toHaveBeenCalledWith(exampleUserData, expect.any(Object)); 122 | expect(mockResponse.send).toHaveBeenCalled(); 123 | expect(mockResponse.json).not.toHaveBeenCalled(); 124 | expect(mockResponse.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); 125 | }); 126 | 127 | it("should handle request show stats and generate SVG response", async () => { 128 | mockRequest.query.username = "FajarKim"; 129 | mockRequest.query.show = "reviews,issues_closed"; 130 | (getData as jest.Mock).mockResolvedValueOnce(exampleUserData); 131 | 132 | await readmeStats(mockRequest, mockResponse); 133 | 134 | expect(getData).toHaveBeenCalledWith(mockRequest.query.username); 135 | expect(card).toHaveBeenCalledWith(exampleUserData, expect.any(Object)); 136 | expect(mockResponse.send).toHaveBeenCalled(); 137 | expect(mockResponse.json).not.toHaveBeenCalled(); 138 | expect(mockResponse.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); 139 | }); 140 | 141 | it("should handle invalid missing username and send error response", async () => { 142 | mockRequest.query.username = undefined; 143 | 144 | await readmeStats(mockRequest, mockResponse); 145 | 146 | expect(getData).not.toHaveBeenCalledWith(); 147 | expect(card).not.toHaveBeenCalledWith(); 148 | expect(mockResponse.send).toHaveBeenCalled(); 149 | expect(mockResponse.json).not.toHaveBeenCalled(); 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /tests/card.test.ts: -------------------------------------------------------------------------------- 1 | import card from "../src/card"; 2 | import locales from "../i18n/index"; 3 | 4 | describe("Test card function", () => { 5 | const mockData = { 6 | username: "FajarKim", 7 | name: "Rangga Fajar Oktariansyah", 8 | picture: "/9j/2wBDADUlKC8oITUvKy88OTU/UIVXUElJUKN1e2GFwarLyL6qurfV8P//1eL/5re6////////////zv//////////////2wBDATk8PFBGUJ1XV53/3Lrc////////////////////////////////////////////////////////////////////wAARCABLAEsDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAQQAAgMF/8QAKhAAAQMDAwMEAQUAAAAAAAAAAQACEQMhMRJBUQQTYSJxkaGxFDIzNIH/xAAWAQEBAQAAAAAAAAAAAAAAAAAAAQL/xAAWEQEBAQAAAAAAAAAAAAAAAAAAARH/2gAMAwEAAhEDEQA/AHFJQS/VmzW7G6yq3fJ6jQ0SOZTCQpHTVZCeQFRBRUFVc4MaXOMAIpbq6rmAMbFxdBenWZUcdJPsVqkaDywgkEg7AJ1QEmBJSVV+uoScCwTVX+MpHVczypVi5/bIy0p1jtTQeQkAc+VZupl2kTwpKuH1EmOpdIBaJKua5aASLHyrsTDKSqnu1jGMLT9RchwcAZgrASBwlpIs21Rh22TiTYwPOkm26bbZoHCQqPEsIXNeYqOHldNLVaAfLcOF2nnwqhYPAIVi8ECSCBxkrItgkE3GwQsIGOVMXVwfUSdgSgXQGo0zFURecoPcHmY0jFgrgs2o4ySSSPwtDUbpJAgHAWDW21Q7/ETc+qZ5JTEbUXkO97J0YSVJpc4abiYTosFItRBzdQ/CKi0hHqpbU9W4wN0uTPAHATfVsL6ovgcLDtefpBVr2gixxHsqkjAwFp2Dz9I9h0Z+kGUtjdXpUjVcdNgMkoigTv8ASY6OmWy4OtMEEINqFLRnay1UwggCKCiBTqamivbJF7rI1vB+Ues/sH2CwUG4rTkGEHVr+kfKx2U2VFzVd4+E/wBID2ASMmVzcuAPK61MBrABgILoKKIP/9k=", 9 | public_repos: 20, 10 | followers: 20, 11 | following: 10, 12 | total_stars: 140, 13 | total_forks: 90, 14 | total_issues: 150, 15 | total_closed_issues: 100, 16 | total_prs: 530, 17 | total_prs_merged: 490, 18 | total_commits: 1330, 19 | total_review: 340, 20 | total_discussion_answered: 4, 21 | total_discussion_started: 10, 22 | total_contributed_to: 12, 23 | }; 24 | 25 | const mockUiConfig = { 26 | Title: "Rangga Fajar Oktariansyah's GitHub Stats", 27 | Locale: "en", 28 | Format: "svg", 29 | titleColor: "2f80ed", 30 | textColor: "434d58", 31 | iconColor: "4c71f2", 32 | borderColor: "e4e2e2", 33 | bgColor: "ffffff", 34 | strokeColor: "e4e2e2", 35 | usernameColor: "2f80ed", 36 | borderRadius: 5, 37 | borderWidth: 1, 38 | hideBorder: false, 39 | hideStroke: false, 40 | disabledAnimations: false, 41 | showItems: "reviews,issues_closed", 42 | hiddenItems: "forks,commits", 43 | Revert: false, 44 | photoQuality: 15, 45 | photoResize: 150, 46 | }; 47 | 48 | it("should generate SVG markup with default values", async () => { 49 | const svgMarkup = await card(mockData, mockUiConfig); 50 | 51 | expect(svgMarkup).toContain("