forked from mirrored/github-readme-stats
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c006a40f92 | ||
|
|
1e2131b19b | ||
|
|
be47a1240a | ||
|
|
b63c10f545 | ||
|
|
7d83fa413d | ||
|
|
d114fd8caf | ||
|
|
0e830c0cc3 |
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"name": "GitHub Readme Stats Dev",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": { "version": "22" }
|
||||
},
|
||||
"forwardPorts": [3000],
|
||||
"portsAttributes": {
|
||||
"3000": { "label": "HTTP" }
|
||||
},
|
||||
"appPort": [],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "npm install -g vercel",
|
||||
|
||||
// Use 'postStartCommand' to run commands after the container is started.
|
||||
"postStartCommand": "hostname dev && npm install",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"yzhang.markdown-all-in-one",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"github.vscode-github-actions"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"remoteUser": "root",
|
||||
"privileged": true
|
||||
}
|
||||
-234
@@ -1,234 +0,0 @@
|
||||
// {
|
||||
// "env": {
|
||||
// "node": true,
|
||||
// "browser": true,
|
||||
// "es2021": true
|
||||
// },
|
||||
// "extends": [
|
||||
// // "eslint:recommended",
|
||||
// "prettier"
|
||||
// ],
|
||||
// "parserOptions": {
|
||||
// "sourceType": "module",
|
||||
// "ecmaVersion": 2022
|
||||
// },
|
||||
// "rules": {
|
||||
// // Possible Errors (overrides from recommended set)
|
||||
|
||||
// // "no-extra-parens": "error",
|
||||
// "no-unexpected-multiline": "error",
|
||||
|
||||
// // All JSDoc comments must be valid
|
||||
|
||||
// "valid-jsdoc": [ "error", {
|
||||
// "requireReturn": true,
|
||||
// "requireReturnDescription": true,
|
||||
// "requireParamDescription": true,
|
||||
// "prefer": {
|
||||
// "return": "returns"
|
||||
// }
|
||||
// }],
|
||||
|
||||
// // Best Practices
|
||||
|
||||
// // Allowed a getter without setter, but all setters require getters
|
||||
|
||||
// "accessor-pairs": [ "error", {
|
||||
// "getWithoutSet": false,
|
||||
// "setWithoutGet": true
|
||||
// }],
|
||||
// "block-scoped-var": "warn",
|
||||
// "consistent-return": "error",
|
||||
// "curly": "error",
|
||||
// // "default-case": "warn",
|
||||
|
||||
// // the dot goes with the property when doing multiline
|
||||
|
||||
// // "dot-location": [ "warn", "property" ],
|
||||
// // "dot-notation": "warn",
|
||||
// // "eqeqeq": [ "error", "smart" ],
|
||||
// // "guard-for-in": "warn",
|
||||
// "no-alert": "error",
|
||||
// "no-caller": "error",
|
||||
// // "no-case-declarations": "warn",
|
||||
// // "no-div-regex": "warn",
|
||||
// // "no-else-return": "warn",
|
||||
// // "no-empty-label": "warn",
|
||||
// // "no-empty-pattern": "warn",
|
||||
// // "no-eq-null": "warn",
|
||||
// // "no-eval": "error",
|
||||
// // "no-extend-native": "error",
|
||||
// // "no-extra-bind": "warn",
|
||||
// // "no-floating-decimal": "warn",
|
||||
// // "no-implicit-coercion": [ "warn", {
|
||||
// // "boolean": true,
|
||||
// // "number": true,
|
||||
// // "string": true
|
||||
// // }],
|
||||
// // "no-implied-eval": "error",
|
||||
// // "no-invalid-this": "error",
|
||||
// // "no-iterator": "error",
|
||||
// // "no-labels": "warn",
|
||||
// // "no-lone-blocks": "warn",
|
||||
// // "no-loop-func": "error",
|
||||
// // "no-magic-numbers": "warn",
|
||||
// // "no-multi-spaces": "error",
|
||||
// // "no-multi-str": "warn",
|
||||
// // "no-native-reassign": "error",
|
||||
// // "no-new-func": "error",
|
||||
// // "no-new-wrappers": "error",
|
||||
// // "no-new": "error",
|
||||
// // "no-octal-escape": "error",
|
||||
// // "no-param-reassign": "error",
|
||||
// // "no-process-env": "warn",
|
||||
// // "no-proto": "error",
|
||||
// // "no-redeclare": "error",
|
||||
// // "no-return-assign": "error",
|
||||
// // "no-script-url": "error",
|
||||
// // "no-self-compare": "error",
|
||||
// // "no-throw-literal": "error",
|
||||
// // "no-unused-expressions": "error",
|
||||
// // "no-useless-call": "error",
|
||||
// // "no-useless-concat": "error",
|
||||
// // "no-void": "warn",
|
||||
|
||||
// // Produce warnings when something is commented as TODO or FIXME
|
||||
|
||||
// "no-warning-comments": [ "warn", {
|
||||
// "terms": [ "TODO", "FIXME" ],
|
||||
// "location": "start"
|
||||
// }],
|
||||
// "no-with": "warn",
|
||||
// "radix": "warn",
|
||||
// // "vars-on-top": "error",
|
||||
|
||||
// // Enforces the style of wrapped functions
|
||||
// // "wrap-iife": [ "error", "outside" ],
|
||||
// // "yoda": "error",
|
||||
|
||||
// // Strict Mode - for ES6, never use strict.
|
||||
// // "strict": [ "error", "never" ],
|
||||
|
||||
// // Variables
|
||||
|
||||
// // "init-declarations": [ "error", "always" ],
|
||||
// // "no-catch-shadow": "warn",
|
||||
// "no-delete-var": "error",
|
||||
// // "no-label-var": "error",
|
||||
// // "no-shadow-restricted-names": "error",
|
||||
// // "no-shadow": "warn",
|
||||
|
||||
// // We require all vars to be initialized (see init-declarations)
|
||||
// // If we NEED a var to be initialized to undefined, it needs to be explicit
|
||||
|
||||
// "no-undef-init": "off",
|
||||
// "no-undef": "error",
|
||||
// "no-undefined": "off",
|
||||
// "no-unused-vars": "warn",
|
||||
|
||||
// // Disallow hoisting - let & const don't allow hoisting anyhow
|
||||
|
||||
// "no-use-before-define": "error",
|
||||
|
||||
// // Node.js and CommonJS
|
||||
|
||||
// // "callback-return": [ "warn", [ "callback", "next" ]],
|
||||
// // "global-require": "error",
|
||||
// // "handle-callback-err": "warn",
|
||||
// // "no-mixed-requires": "warn",
|
||||
// // "no-new-require": "error",
|
||||
|
||||
// // Use path.concat instead
|
||||
|
||||
// // "no-path-concat": "error",
|
||||
// // "no-process-exit": "error",
|
||||
// // "no-restricted-modules": "off",
|
||||
// // "no-sync": "warn",
|
||||
|
||||
// // ECMAScript 6 support
|
||||
|
||||
// // "arrow-body-style": [ "error", "always" ],
|
||||
// // "arrow-parens": [ "error", "always" ],
|
||||
// // "arrow-spacing": [ "error", { "before": true, "after": true }],
|
||||
// "constructor-super": "error",
|
||||
// // "generator-star-spacing": [ "error", "before" ],
|
||||
// // "no-arrow-condition": "error",
|
||||
// "no-class-assign": "error",
|
||||
// "no-const-assign": "error",
|
||||
// "no-dupe-class-members": "error",
|
||||
// "no-this-before-super": "error",
|
||||
// // "no-var": "warn",
|
||||
// "object-shorthand": [ "warn" ],
|
||||
// // "prefer-arrow-callback": "warn",
|
||||
// // "prefer-spread": "warn",
|
||||
// // "prefer-template": "warn",
|
||||
// // "require-yield": "error",
|
||||
|
||||
// // Stylistic - everything here is a warning because of style.
|
||||
|
||||
// // "array-bracket-spacing": [ "warn", "always" ],
|
||||
// // "block-spacing": [ "warn", "always" ],
|
||||
// // "brace-style": [ "warn", "1tbs", { "allowSingleLine": false } ],
|
||||
// // "camelcase": "warn",
|
||||
// // "comma-spacing": [ "warn", { "before": false, "after": true } ],
|
||||
// // "comma-style": [ "warn", "last" ],
|
||||
// // "computed-property-spacing": [ "warn", "never" ],
|
||||
// // "consistent-this": [ "warn", "self" ],
|
||||
// // "eol-last": "warn",
|
||||
// // "func-names": "warn",
|
||||
// // "func-style": [ "warn", "declaration" ],
|
||||
// // "id-length": [ "warn", { "min": 2, "max": 32 } ],
|
||||
// // "indent": [ "warn", 4 ],
|
||||
// // "jsx-quotes": [ "warn", "prefer-double" ],
|
||||
// // "linebreak-style": [ "warn", "unix" ],
|
||||
// // "lines-around-comment": [ "warn", { "beforeBlockComment": true } ],
|
||||
// // "max-depth": [ "warn", 8 ],
|
||||
// // "max-len": [ "warn", 132 ],
|
||||
// // "max-nested-callbacks": [ "warn", 8 ],
|
||||
// // "max-params": [ "warn", 8 ],
|
||||
// // "new-cap": "warn",
|
||||
// // "new-parens": "warn",
|
||||
// // "no-array-constructor": "warn",
|
||||
// // "no-bitwise": "off",
|
||||
// // "no-continue": "off",
|
||||
// // "no-inline-comments": "off",
|
||||
// // "no-lonely-if": "warn",
|
||||
// "no-mixed-spaces-and-tabs": "warn",
|
||||
// "no-multiple-empty-lines": "warn",
|
||||
// "no-negated-condition": "warn",
|
||||
// // "no-nested-ternary": "warn",
|
||||
// // "no-new-object": "warn",
|
||||
// // "no-plusplus": "off",
|
||||
// // "no-spaced-func": "warn",
|
||||
// // "no-ternary": "off",
|
||||
// // "no-trailing-spaces": "warn",
|
||||
// // "no-underscore-dangle": "warn",
|
||||
// "no-unneeded-ternary": "warn",
|
||||
// // "object-curly-spacing": [ "warn", "always" ],
|
||||
// // "one-var": "off",
|
||||
// // "operator-assignment": [ "warn", "never" ],
|
||||
// // "operator-linebreak": [ "warn", "after" ],
|
||||
// // "padded-blocks": [ "warn", "never" ],
|
||||
// // "quote-props": [ "warn", "consistent-as-needed" ],
|
||||
// // "quotes": [ "warn", "single" ],
|
||||
// "require-jsdoc": [ "warn", {
|
||||
// "require": {
|
||||
// "FunctionDeclaration": true,
|
||||
// "MethodDefinition": true,
|
||||
// "ClassDeclaration": false
|
||||
// }
|
||||
// }],
|
||||
// // "semi-spacing": [ "warn", { "before": false, "after": true }],
|
||||
// // "semi": [ "error", "always" ],
|
||||
// // "sort-vars": "off",
|
||||
// "keyword-spacing": ["error", { "before": true, "after": true }]
|
||||
// // "space-before-blocks": [ "warn", "always" ],
|
||||
// // "space-before-function-paren": [ "warn", "never" ],
|
||||
// // "space-in-parens": [ "warn", "never" ],
|
||||
// // "space-infix-ops": [ "warn", { "int32Hint": true } ],
|
||||
// // "space-return-throw-case": "error",
|
||||
// // "space-unary-ops": "error",
|
||||
// // "spaced-comment": [ "warn", "always" ],
|
||||
// // "wrap-regex": "warn"
|
||||
// }
|
||||
// }
|
||||
@@ -1,34 +0,0 @@
|
||||
name: Push Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'develop'
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
${{ vars.DOCKERHUB_TARGET }}:develop
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
name: Push Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9.]+-[0-9]+-[0-9]+'
|
||||
# for this project, we do "YYYY-MM-DD" for version numbers, due to unversioned upstream
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
${{ vars.DOCKERHUB_TARGET }}:latest
|
||||
${{ vars.DOCKERHUB_TARGET }}:${{ github.ref_name }}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
name: Sync GitHub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config --global user.name "Andrew Sync"
|
||||
git config --global user.email "sync@digitaladapt.com"
|
||||
|
||||
- name: Add GitHub Remote
|
||||
env:
|
||||
SYNC_TOKEN: ${{ secrets.SYNC_GITHUB_TOKEN }}
|
||||
SYNC_TARGET: ${{ vars.SYNC_GITHUB_TARGET }}
|
||||
run: |
|
||||
git remote add github "https://digitaladapt:${SYNC_TOKEN}@github.com/$SYNC_TARGET"
|
||||
|
||||
- name: Push Current Branch
|
||||
run: |
|
||||
git push github HEAD:${GITHUB_REF_NAME}
|
||||
|
||||
- name: Push Tags
|
||||
run: |
|
||||
git push github --tags
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# This file is used to define code owners for the repository.
|
||||
# Code owners are automatically requested for review when someone opens a pull request that modifies code they own.
|
||||
|
||||
# Assign @qwerty541 as the owner for package.json and package-lock.json
|
||||
package.json @qwerty541
|
||||
package-lock.json @qwerty541
|
||||
+2
-5
@@ -1,6 +1,6 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [anuraghazra] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
@@ -9,7 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: [
|
||||
"https://www.paypal.me/anuraghazra",
|
||||
"https://www.buymeacoffee.com/anuraghazra",
|
||||
] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
custom: ["https://www.paypal.me/anuraghazra", "https://www.buymeacoffee.com/anuraghazra"] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots / Live demo link (paste the github-readme-stats link as markdown image)**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -1,60 +0,0 @@
|
||||
name: Bug report
|
||||
description: Create a report to help us improve.
|
||||
labels:
|
||||
- "bug"
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
:warning: PLEASE FIRST READ THE FAQ [(#1770)](https://github.com/anuraghazra/github-readme-stats/discussions/1770) AND COMMON ERROR CODES [(#1772)](https://github.com/anuraghazra/github-readme-stats/issues/1772)!!!
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description:
|
||||
A clear and concise description of what you expected to happen.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Screenshots / Live demo link
|
||||
description: If applicable, add screenshots to help explain your problem.
|
||||
placeholder: Paste the github-readme-stats link as markdown image
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
---
|
||||
### FAQ (Snippet)
|
||||
|
||||
Below are some questions that are found in the FAQ. The full FAQ can be found in [#1770](https://github.com/anuraghazra/github-readme-stats/discussions/1770).
|
||||
|
||||
#### Q: My card displays an error
|
||||
|
||||
**Ans:** First, check the common error codes (i.e. https://github.com/anuraghazra/github-readme-stats/issues/1772) and existing issues before creating a new one.
|
||||
|
||||
#### Q: How to hide jupyter Notebook?
|
||||
|
||||
**Ans:** `&hide=jupyter%20notebook`.
|
||||
|
||||
#### Q: I could not figure out how to deploy on my own vercel instance
|
||||
|
||||
**Ans:** Please check:
|
||||
- Docs: https://github.com/anuraghazra/github-readme-stats/#deploy-on-your-own-vercel-instance
|
||||
- YT tutorial by codeSTACKr: https://www.youtube.com/watch?v=n6d4KHSKqGk&feature=youtu.be&t=107
|
||||
|
||||
#### Q: Language Card is incorrect
|
||||
|
||||
**Ans:** Please read these issues/comments before opening any issues regarding language card stats:
|
||||
- https://github.com/anuraghazra/github-readme-stats/issues/136#issuecomment-665164174
|
||||
- https://github.com/anuraghazra/github-readme-stats/issues/136#issuecomment-665172181
|
||||
|
||||
#### Q: How to count private stats?
|
||||
|
||||
**Ans:** We can only count private commits & we cannot access any other private info of any users, so it's impossible. The only way is to deploy on your own instance & use your own PAT (Personal Access Token).
|
||||
@@ -1,12 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Question
|
||||
url: https://github.com/anuraghazra/github-readme-stats/discussions
|
||||
about: Please ask and answer questions here.
|
||||
- name: Error
|
||||
url: https://github.com/anuraghazra/github-readme-stats/issues/1772
|
||||
about:
|
||||
Before opening a bug report, please check the 'Common Error Codes' issue.
|
||||
- name: FAQ
|
||||
url: https://github.com/anuraghazra/github-readme-stats/discussions/1770
|
||||
about: Please first check the FAQ before asking a question.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -1,28 +0,0 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea for this project.
|
||||
labels:
|
||||
- "enhancement"
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description:
|
||||
A clear and concise description of what the problem is. Ex. I'm always
|
||||
frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description:
|
||||
A clear and concise description of any alternative solutions or features
|
||||
you've considered.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description:
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -1,31 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Maintain dependencies for npm
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
commit-message:
|
||||
prefix: "build(deps)"
|
||||
prefix-development: "build(deps-dev)"
|
||||
|
||||
# Maintain dependencies for GitHub Actions
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
commit-message:
|
||||
prefix: "ci(deps)"
|
||||
prefix-development: "ci(deps-dev)"
|
||||
|
||||
# Maintain dependencies for Devcontainers
|
||||
- package-ecosystem: devcontainers
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
commit-message:
|
||||
prefix: "build(deps)"
|
||||
prefix-development: "build(deps-dev)"
|
||||
@@ -1,90 +0,0 @@
|
||||
themes:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- themes/index.js
|
||||
|
||||
card-i18n:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- src/translations.js
|
||||
- src/common/I18n.js
|
||||
|
||||
documentation:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- readme.md
|
||||
- CONTRIBUTING.md
|
||||
- CODE_OF_CONDUCT.md
|
||||
- SECURITY.md
|
||||
|
||||
dependencies:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- package.json
|
||||
- package-lock.json
|
||||
|
||||
lang-card:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- api/top-langs.js
|
||||
- src/cards/top-languages.js
|
||||
- src/fetchers/top-languages.js
|
||||
- tests/fetchTopLanguages.test.js
|
||||
- tests/renderTopLanguagesCard.test.js
|
||||
- tests/top-langs.test.js
|
||||
|
||||
repo-card:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- api/pin.js
|
||||
- src/cards/repo.js
|
||||
- src/fetchers/repo.js
|
||||
- tests/fetchRepo.test.js
|
||||
- tests/renderRepoCard.test.js
|
||||
- tests/pin.test.js
|
||||
|
||||
stats-card:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- api/index.js
|
||||
- src/cards/stats.js
|
||||
- src/fetchers/stats.js
|
||||
- tests/fetchStats.test.js
|
||||
- tests/renderStatsCard.test.js
|
||||
- tests/api.test.js
|
||||
|
||||
wakatime-card:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- api/wakatime.js
|
||||
- src/cards/wakatime.js
|
||||
- src/fetchers/wakatime.js
|
||||
- tests/fetchWakatime.test.js
|
||||
- tests/renderWakatimeCard.test.js
|
||||
- tests/wakatime.test.js
|
||||
|
||||
gist-card:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- api/gist.js
|
||||
- src/cards/gist.js
|
||||
- src/fetchers/gist.js
|
||||
- tests/fetchGist.test.js
|
||||
- tests/renderGistCard.test.js
|
||||
- tests/gist.test.js
|
||||
|
||||
ranks:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- src/calculateRank.js
|
||||
|
||||
ci:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- .github/workflows/*
|
||||
- scripts/*
|
||||
|
||||
infrastructure:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- .eslintrc.json
|
||||
@@ -1,20 +0,0 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 30
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- feature
|
||||
- enhancement
|
||||
- help wanted
|
||||
- bug
|
||||
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
@@ -1,43 +0,0 @@
|
||||
name: "Static code analysis workflow (CodeQL)"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
checks: read
|
||||
contents: read
|
||||
deployments: read
|
||||
issues: read
|
||||
discussions: read
|
||||
packages: read
|
||||
pages: read
|
||||
pull-requests: read
|
||||
repository-projects: read
|
||||
security-events: write
|
||||
statuses: read
|
||||
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
|
||||
# CodeQL runs on ubuntu-latest, windows-latest, and macos-latest
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@46a6823b81f2d7c67ddf123851eea88365bc8a67 # v2.13.5
|
||||
with:
|
||||
languages: javascript
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@46a6823b81f2d7c67ddf123851eea88365bc8a67 # v2.13.5
|
||||
@@ -1,10 +0,0 @@
|
||||
import os
|
||||
|
||||
file = open('./vercel.json', 'r')
|
||||
str = file.read()
|
||||
file = open('./vercel.json', 'w')
|
||||
|
||||
str = str.replace('"maxDuration": 10', '"maxDuration": 15')
|
||||
|
||||
file.write(str)
|
||||
file.close()
|
||||
@@ -1,20 +0,0 @@
|
||||
name: Deployment Prep
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
config:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Deployment Prep
|
||||
run: python ./.github/workflows/deploy-prep.py
|
||||
- uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0
|
||||
with:
|
||||
branch: vercel
|
||||
create_branch: true
|
||||
push_options: "--force"
|
||||
@@ -1,41 +0,0 @@
|
||||
name: Test Deployment
|
||||
on:
|
||||
# Temporarily disabled automatic triggers; manual-only for now.
|
||||
workflow_dispatch:
|
||||
# Original trigger (restore to re-enable):
|
||||
# deployment_status:
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
e2eTests:
|
||||
# Temporarily disabled; set to the original condition to re-enable.
|
||||
# if:
|
||||
# github.repository == 'anuraghazra/github-readme-stats' &&
|
||||
# github.event_name == 'deployment_status' &&
|
||||
# github.event.deployment_status.state == 'success'
|
||||
if: false
|
||||
name: Perform e2e tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Run end-to-end tests.
|
||||
run: npm run test:e2e
|
||||
# env:
|
||||
# VERCEL_PREVIEW_URL: ${{ github.event.deployment_status.target_url }}
|
||||
@@ -1,47 +0,0 @@
|
||||
name: Close empty issues and templates
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- reopened
|
||||
- opened
|
||||
- edited
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
checks: read
|
||||
contents: read
|
||||
deployments: read
|
||||
issues: write
|
||||
discussions: read
|
||||
packages: read
|
||||
pages: read
|
||||
pull-requests: read
|
||||
repository-projects: read
|
||||
security-events: read
|
||||
statuses: read
|
||||
|
||||
jobs:
|
||||
closeEmptyIssuesAndTemplates:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
name: Close empty issues
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# NOTE: Retrieve issue templates.
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Run empty issues closer action
|
||||
uses: rickstaa/empty-issues-closer-action@e96914613221511279ca25f50fd4acc85e331d99 # v1.1.74
|
||||
env:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
close_comment:
|
||||
Closing this issue because it appears to be empty. Please update the
|
||||
issue for it to be reopened.
|
||||
open_comment:
|
||||
Reopening this issue because the author provided more information.
|
||||
check_templates: true
|
||||
template_close_comment:
|
||||
Closing this issue since the issue template was not filled in.
|
||||
Please provide us with more information to have this issue reopened.
|
||||
template_open_comment:
|
||||
Reopening this issue because the author provided more information.
|
||||
@@ -1,56 +1,32 @@
|
||||
name: Generate Theme Readme
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "themes/index.js"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
checks: read
|
||||
contents: write
|
||||
deployments: read
|
||||
issues: read
|
||||
discussions: read
|
||||
packages: read
|
||||
pages: read
|
||||
pull-requests: read
|
||||
repository-projects: read
|
||||
security-events: read
|
||||
statuses: read
|
||||
|
||||
jobs:
|
||||
generateThemeDoc:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Generate theme doc
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
- uses: actions/checkout@v1
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
|
||||
# Fix the unsafe repo error which was introduced by the CVE-2022-24765 git patches.
|
||||
- name: Fix unsafe repo error
|
||||
run: git config --global --add safe.directory ${{ github.workspace }}
|
||||
node-version: "12.x"
|
||||
|
||||
- name: npm install, generate readme
|
||||
run: |
|
||||
npm ci
|
||||
npm install
|
||||
npm run theme-readme-gen
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Run Script
|
||||
uses: skx/github-action-tester@e29768ff4ff67be9d1fdbccd8836ab83233bebb1 # v0.10.0
|
||||
uses: skx/github-action-tester@master
|
||||
with:
|
||||
script: ./scripts/push-theme-readme.sh
|
||||
env:
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
name: "Pull Request Labeler"
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
checks: read
|
||||
contents: read
|
||||
deployments: read
|
||||
issues: read
|
||||
discussions: read
|
||||
packages: read
|
||||
pages: read
|
||||
pull-requests: write
|
||||
repository-projects: read
|
||||
security-events: read
|
||||
statuses: read
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
sync-labels: true
|
||||
@@ -1,49 +0,0 @@
|
||||
name: OSSF Scorecard analysis workflow
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
name: Scorecard analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed if using Code scanning alerts
|
||||
security-events: write
|
||||
# Needed for GitHub OIDC token if publish_results is true
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
publish_results: true
|
||||
|
||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||
# format to the repository Actions tab.
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
|
||||
# required for Code scanning alerts
|
||||
- name: "Upload SARIF results to code scanning"
|
||||
uses: github/codeql-action/upload-sarif@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
@@ -1,50 +1,30 @@
|
||||
name: Theme preview
|
||||
on:
|
||||
# Temporary disabled due to paused themes addition.
|
||||
# See: https://github.com/anuraghazra/github-readme-stats/issues/3404
|
||||
# pull_request_target:
|
||||
# types: [opened, edited, reopened, synchronize]
|
||||
# branches:
|
||||
# - master
|
||||
# paths:
|
||||
# - "themes/index.js"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
checks: read
|
||||
contents: read
|
||||
deployments: read
|
||||
issues: read
|
||||
discussions: read
|
||||
packages: read
|
||||
pages: read
|
||||
pull-requests: write
|
||||
repository-projects: read
|
||||
security-events: read
|
||||
statuses: read
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
branches:
|
||||
- master
|
||||
- theme-preview-script
|
||||
- "themes/index.js"
|
||||
|
||||
jobs:
|
||||
previewTheme:
|
||||
name: Install & Preview
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
- uses: actions/checkout@v1
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
node-version: "12.x"
|
||||
|
||||
- uses: bahmutov/npm-install@3e063b974f0d209807684aa23e534b3dde517fd9 # v1.11.2
|
||||
with:
|
||||
useLockFile: false
|
||||
|
||||
- run: npm run preview-theme
|
||||
- name: npm install, preview theme
|
||||
run: |
|
||||
npm install
|
||||
npm run preview-theme
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CI: true
|
||||
PERSONAL_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
name: Cleanup closed pull requests cache
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
checks: read
|
||||
contents: read
|
||||
deployments: read
|
||||
issues: read
|
||||
discussions: read
|
||||
packages: read
|
||||
pages: read
|
||||
pull-requests: read
|
||||
repository-projects: read
|
||||
security-events: read
|
||||
statuses: read
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cleanup
|
||||
run: |
|
||||
gh extension install actions/gh-actions-cache
|
||||
|
||||
REPO=${{ github.repository }}
|
||||
BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge"
|
||||
|
||||
echo "Fetching list of cache key"
|
||||
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
|
||||
|
||||
## Setting this to not fail the workflow while deleting cache keys.
|
||||
set +e
|
||||
echo "Deleting caches..."
|
||||
for cacheKey in $cacheKeysForPR
|
||||
do
|
||||
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
|
||||
done
|
||||
echo "Done"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,57 +0,0 @@
|
||||
name: Close stale theme pull requests that have the 'invalid' label.
|
||||
on:
|
||||
# Temporary disabled due to paused themes addition.
|
||||
# See: https://github.com/anuraghazra/github-readme-stats/issues/3404
|
||||
# schedule:
|
||||
# # ┌───────────── minute (0 - 59)
|
||||
# # │ ┌───────────── hour (0 - 23)
|
||||
# # │ │ ┌───────────── day of the month (1 - 31)
|
||||
# # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
|
||||
# # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
|
||||
# # │ │ │ │ │
|
||||
# # │ │ │ │ │
|
||||
# # │ │ │ │ │
|
||||
# # * * * * *
|
||||
# - cron: "0 0 */7 * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
checks: read
|
||||
contents: read
|
||||
deployments: read
|
||||
issues: read
|
||||
discussions: read
|
||||
packages: read
|
||||
pages: read
|
||||
pull-requests: write
|
||||
repository-projects: read
|
||||
security-events: read
|
||||
statuses: read
|
||||
|
||||
jobs:
|
||||
closeOldThemePrs:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
name: Close stale 'invalid' theme PRs
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
|
||||
- uses: bahmutov/npm-install@3e063b974f0d209807684aa23e534b3dde517fd9 # v1.11.2
|
||||
with:
|
||||
useLockFile: false
|
||||
|
||||
- run: npm run close-stale-theme-prs
|
||||
env:
|
||||
STALE_DAYS: 20
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
+17
-25
@@ -1,47 +1,39 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- "*"
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Perform tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
node-version: "12.x"
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-npm-cache-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-npm-cache-
|
||||
|
||||
- name: Install & Test
|
||||
run: |
|
||||
npm ci
|
||||
npm install
|
||||
npm run test
|
||||
|
||||
- name: Run ESLint
|
||||
run: |
|
||||
npm run lint
|
||||
|
||||
- name: Run bench tests
|
||||
run: |
|
||||
npm run bench
|
||||
|
||||
- name: Run Prettier
|
||||
run: |
|
||||
npm run format:check
|
||||
|
||||
- name: Code Coverage
|
||||
uses: codecov/codecov-action@4fe8c5f003fae66aa5ebb77cfd3e7bfbbda0b6b0 # v3.1.5
|
||||
uses: codecov/codecov-action@v1
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
name: Theme Pull Requests Closer
|
||||
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
checks: read
|
||||
contents: read
|
||||
deployments: read
|
||||
issues: read
|
||||
discussions: read
|
||||
packages: read
|
||||
pages: read
|
||||
pull-requests: write
|
||||
repository-projects: read
|
||||
security-events: read
|
||||
statuses: read
|
||||
|
||||
jobs:
|
||||
close-prs:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up Git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Close Pull Requests
|
||||
run: |
|
||||
comment_message="We are currently pausing addition of new themes. If this theme is exclusively for your personal use, then instead of adding it to our theme collection, you can use card [customization options](https://github.com/anuraghazra/github-readme-stats?tab=readme-ov-file#customization)."
|
||||
|
||||
for pr_number in $(gh pr list -l "themes" -q is:open --json number -q ".[].number"); do
|
||||
gh pr close $pr_number -c "$comment_message"
|
||||
done
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,49 +0,0 @@
|
||||
name: Update top issues dashboard
|
||||
on:
|
||||
schedule:
|
||||
# ┌───────────── minute (0 - 59)
|
||||
# │ ┌───────────── hour (0 - 23)
|
||||
# │ │ ┌───────────── day of the month (1 - 31)
|
||||
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
|
||||
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
|
||||
# │ │ │ │ │
|
||||
# │ │ │ │ │
|
||||
# │ │ │ │ │
|
||||
# * * * * *
|
||||
- cron: "0 0 */3 * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
checks: read
|
||||
contents: read
|
||||
deployments: read
|
||||
issues: write
|
||||
discussions: read
|
||||
packages: read
|
||||
pages: read
|
||||
pull-requests: write
|
||||
repository-projects: read
|
||||
security-events: read
|
||||
statuses: read
|
||||
|
||||
jobs:
|
||||
showAndLabelTopIssues:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
name: Update top issues Dashboard.
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Run top issues action
|
||||
uses: rickstaa/top-issues-action@7e8dda5d5ae3087670f9094b9724a9a091fc3ba1 # v1.3.101
|
||||
env:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
top_list_size: 10
|
||||
filter: "1772"
|
||||
label: true
|
||||
dashboard: true
|
||||
dashboard_show_total_reactions: true
|
||||
top_issues: true
|
||||
top_bugs: true
|
||||
top_features: true
|
||||
top_pull_requests: true
|
||||
@@ -1,67 +0,0 @@
|
||||
name: Update supported languages
|
||||
on:
|
||||
schedule:
|
||||
# ┌───────────── minute (0 - 59)
|
||||
# │ ┌───────────── hour (0 - 23)
|
||||
# │ │ ┌───────────── day of the month (1 - 31)
|
||||
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
|
||||
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
|
||||
# │ │ │ │ │
|
||||
# │ │ │ │ │
|
||||
# │ │ │ │ │
|
||||
# * * * * *
|
||||
- cron: "0 0 */30 * *"
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
checks: read
|
||||
contents: write
|
||||
deployments: read
|
||||
issues: read
|
||||
discussions: read
|
||||
packages: read
|
||||
pages: read
|
||||
pull-requests: write
|
||||
repository-projects: read
|
||||
security-events: read
|
||||
statuses: read
|
||||
|
||||
jobs:
|
||||
updateLanguages:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
name: Update supported languages
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Run update-languages-json.js script
|
||||
run: npm run generate-langs-json
|
||||
|
||||
- name: Create Pull Request if upstream language file is changed
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
||||
with:
|
||||
commit-message: "refactor: update languages JSON"
|
||||
branch: "update_langs/patch"
|
||||
delete-branch: true
|
||||
title: Update languages JSON
|
||||
body:
|
||||
"The
|
||||
[update-langs](https://github.com/anuraghazra/github-readme-stats/actions/workflows/update-langs.yaml)
|
||||
action found new/updated languages in the [upstream languages JSON
|
||||
file](https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml)."
|
||||
labels: "ci, lang-card"
|
||||
+3
-12
@@ -1,16 +1,7 @@
|
||||
.vercel
|
||||
host.env
|
||||
.env
|
||||
node_modules
|
||||
package-lock.json
|
||||
*.lock
|
||||
.idea/
|
||||
.vscode/
|
||||
coverage
|
||||
benchmarks
|
||||
vercel_token
|
||||
|
||||
# IDE
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/settings.json
|
||||
*.code-workspace
|
||||
|
||||
.vercel
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
_
|
||||
@@ -1,3 +0,0 @@
|
||||
npm test
|
||||
npm run lint
|
||||
npx lint-staged
|
||||
@@ -1,5 +0,0 @@
|
||||
node_modules
|
||||
*.json
|
||||
*.md
|
||||
coverage
|
||||
.vercel
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"trailingComma": "all",
|
||||
"useTabs": false,
|
||||
"endOfLine": "auto",
|
||||
"proseWrap": "always"
|
||||
}
|
||||
+2
-14
@@ -1,15 +1,3 @@
|
||||
.devcontainer
|
||||
.github
|
||||
.husky
|
||||
.vscode
|
||||
benchmarks
|
||||
coverage
|
||||
scripts
|
||||
tests
|
||||
.env
|
||||
**/*.md
|
||||
**/*.svg
|
||||
.eslintrc.json
|
||||
.prettierignore
|
||||
.pretterrc.json
|
||||
codecov.yml
|
||||
package-lock.json
|
||||
coverage
|
||||
Vendored
-9
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"yzhang.markdown-all-in-one",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"ms-azuretools.vscode-containers",
|
||||
"github.vscode-github-actions"
|
||||
]
|
||||
}
|
||||
Vendored
-8
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"markdown.extension.toc.levels": "1..3",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[javascript]": {
|
||||
"editor.tabSize": 2
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at hazru.anurag@gmail.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
+37
-73
@@ -2,103 +2,63 @@
|
||||
|
||||
We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
|
||||
|
||||
- Reporting [an issue](https://github.com/anuraghazra/github-readme-stats/issues/new?assignees=&labels=bug&template=bug_report.yml).
|
||||
- [Discussing](https://github.com/anuraghazra/github-readme-stats/discussions) the current state of the code.
|
||||
- Submitting [a fix](https://github.com/anuraghazra/github-readme-stats/compare).
|
||||
- Proposing [new features](https://github.com/anuraghazra/github-readme-stats/issues/new?assignees=&labels=enhancement&template=feature_request.yml).
|
||||
- Becoming a maintainer.
|
||||
- Reporting a issue
|
||||
- Discussing the current state of the code
|
||||
- Submitting a fix
|
||||
- Proposing new features
|
||||
- Becoming a maintainer
|
||||
|
||||
## All Changes Happen Through Pull Requests
|
||||
|
||||
Pull requests are the best way to propose changes. We actively welcome your pull requests:
|
||||
|
||||
1. Fork the repo and create your branch from `master`.
|
||||
2. If you've added code that should be tested, add some tests' examples.
|
||||
3. If you've changed APIs, update the documentation.
|
||||
4. Issue that pull request!
|
||||
|
||||
## Under the hood of github-readme-stats
|
||||
|
||||
Interested in diving deeper into understanding how github-readme-stats works?
|
||||
|
||||
[Bohdan](https://github.com/Bogdan-Lyashenko) wrote a fantastic in-depth post about it, check it out:
|
||||
|
||||
**[Under the hood of github-readme-stats project](https://codecrumbs.io/library/github-readme-stats)**
|
||||
1. Fork the repo and create your branch from `master`.
|
||||
1. If you've added code that should be tested, add some tests' example.
|
||||
1. If you've changed APIs, update the documentation.
|
||||
1. Issue that pull request!
|
||||
|
||||
## Local Development
|
||||
|
||||
To run & test github-readme-stats, you need to follow a few simple steps:-
|
||||
_(make sure you already have a [Vercel](https://vercel.com/) account)_
|
||||
To run & test github-readme-stats you need to follow few simple steps :-
|
||||
_(make sure you already have a [vercel](https://vercel.com/) account)_
|
||||
|
||||
1. Install [Vercel CLI](https://vercel.com/download).
|
||||
2. Fork the repository and clone the code to your local machine.
|
||||
3. Run `npm install` in the repository root.
|
||||
4. Run the command `vercel` in the root and follow the steps there.
|
||||
5. Run the command `vercel dev` to start a development server at <http://localhost:3000>.
|
||||
6. Create a `.env` file in the root and add the following line `NODE_ENV=development`, this will disable caching for local development.
|
||||
7. The cards will then be available from this local endpoint (i.e. `http://localhost:3000/api?username=anuraghazra`).
|
||||
|
||||
> [!NOTE]
|
||||
> You can debug the package code in [Vscode](https://code.visualstudio.com/) by using the [Node.js: Attach to process](https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_setting-up-an-attach-configuration) debug option. You can also debug any tests using the [VSCode Jest extension](https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest). For more information, see https://github.com/jest-community/vscode-jest/issues/912.
|
||||
1. Install [Vercel CLI](https://vercel.com/download)
|
||||
1. Fork the repository and clone the code to your local machine
|
||||
1. Run the command "vercel" in the root and follow the steps there
|
||||
1. Create a `.env` file in the root of the directory
|
||||
1. In the .env file add a new variable named "PAT" with your [github Personal access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token)
|
||||
1. Run the command "vercel dev" to start a development server at https://localhost:3000
|
||||
|
||||
## Themes Contribution
|
||||
|
||||
We're currently paused addition of new themes to decrease maintenance efforts. All pull requests related to new themes will be closed.
|
||||
GitHub Readme Stats supports custom theming and you can also contribute new themes!
|
||||
|
||||
> [!NOTE]
|
||||
> If you are considering contributing your theme just because you are using it personally, then instead of adding it to our theme collection, you can use card [customization options](./readme.md#customization).
|
||||
All you need to do is edit [themes/index.js](./themes/index.js) file and add your theme at the end of the file.
|
||||
|
||||
## Translations Contribution
|
||||
While creating the Pull request to add a new theme **don't forget to add a screenshot of how your theme looks**, you can also test how it looks using custom url parameters like `title_color`, `icon_color`, `bg_color`, `text_color`
|
||||
|
||||
GitHub Readme Stats 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).
|
||||
|
||||
To contribute your language you need to edit the [src/translations.js](./src/translations.js) file and add new property to each object where the key is the language code in [ISO 639-1 standard](https://www.andiamo.co.uk/resources/iso-language-codes/) and the value is the translated string.
|
||||
> NOTE: If you are contributing your theme just because you are using it personally, then you can [customize the looks](./readme.md#customization) of your card with URL params instead.
|
||||
|
||||
## Any contributions you make will be under the MIT Software License
|
||||
|
||||
In short, when you submit changes, your submissions are understood to be under the same [MIT License](https://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
|
||||
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.
|
||||
|
||||
## Report issues/bugs using GitHub's [issues](https://github.com/anuraghazra/github-readme-stats/issues)
|
||||
|
||||
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/anuraghazra/github-readme-stats/issues/new/choose); it's that easy!
|
||||
|
||||
## Frequently Asked Questions (FAQs)
|
||||
|
||||
**Q:** How to hide Jupyter Notebook?
|
||||
|
||||
> **Ans:** &hide=jupyter%20notebook
|
||||
|
||||
**Q:** I could not figure out how to deploy on my own Vercel instance
|
||||
|
||||
> **Ans:**
|
||||
>
|
||||
> - docs: <https://github.com/anuraghazra/github-readme-stats/#deploy-on-your-own-vercel-instance>
|
||||
> - YT tutorial by codeSTACKr: <https://www.youtube.com/watch?v=n6d4KHSKqGk&feature=youtu.be&t=107>
|
||||
|
||||
**Q:** Language Card is incorrect
|
||||
|
||||
> **Ans:** Please read all the related issues/comments before opening any issues regarding language card stats:
|
||||
>
|
||||
> - <https://github.com/anuraghazra/github-readme-stats/issues/136#issuecomment-665164174>
|
||||
>
|
||||
> - <https://github.com/anuraghazra/github-readme-stats/issues/136#issuecomment-665172181>
|
||||
|
||||
**Q:** How to count private stats?
|
||||
|
||||
> **Ans:** We can only count public commits & we cannot access any other private info of any users, so it's not possible. The only way to count your personal private stats is to deploy on your own instance & use your own PAT (Personal Access Token)
|
||||
|
||||
### Bug Reports
|
||||
|
||||
**Great Bug Reports** tend to have:
|
||||
|
||||
- A quick summary and/or background
|
||||
- Steps to reproduce
|
||||
- Be specific!
|
||||
- Share the snapshot, if possible.
|
||||
- GitHub Readme Stats' live link
|
||||
- What actually happens
|
||||
- What you expected would happen
|
||||
- Notes (possibly including why you think this might be happening or stuff you tried that didn't work)
|
||||
- A quick summary and/or background
|
||||
- Steps to reproduce
|
||||
- Be specific!
|
||||
- Share the snapshot, if possible.
|
||||
- GitHub Readme Stats' live link
|
||||
- What actually happens
|
||||
- What you expected would happen
|
||||
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
|
||||
|
||||
People _love_ thorough bug reports. I'm not even kidding.
|
||||
|
||||
@@ -106,6 +66,10 @@ People _love_ thorough bug reports. I'm not even kidding.
|
||||
|
||||
**Great Feature Requests** tend to have:
|
||||
|
||||
- A quick idea summary
|
||||
- What & why do you want to add the specific feature
|
||||
- Additional context like images, links to resources to implement the feature, etc.
|
||||
- A quick idea summary
|
||||
- What & why you wanted to add the specific feature
|
||||
- Additional Context like images, links to resources to implement the feature etc etc.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under its [MIT License](./LICENSE).
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
readme-stats.example.com {
|
||||
reverse_proxy readme_stats:9000
|
||||
}
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
FROM node:lts-alpine
|
||||
|
||||
# setup folder within the docker container with this app's files
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
# add express.js as dependency and download node modules
|
||||
RUN npm install express.js
|
||||
|
||||
# start the app, on the supplied port
|
||||
EXPOSE $port
|
||||
CMD [ "node", "express.js" ]
|
||||
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
# GitHub Readme Stats Security Policies and Procedures <!-- omit in toc -->
|
||||
|
||||
This document outlines security procedures and general policies for the
|
||||
GitHub Readme Stats project.
|
||||
|
||||
- [Reporting a Vulnerability](#reporting-a-vulnerability)
|
||||
- [Disclosure Policy](#disclosure-policy)
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
The GitHub Readme Stats team and community take all security vulnerabilities
|
||||
seriously. Thank you for improving the security of our open source
|
||||
software. We appreciate your efforts and responsible disclosure and will
|
||||
make every effort to acknowledge your contributions.
|
||||
|
||||
Report security vulnerabilities by emailing the GitHub Readme Stats team at:
|
||||
|
||||
```
|
||||
hazru.anurag@gmail.com
|
||||
```
|
||||
|
||||
The lead maintainer will acknowledge your email within 24 hours, and will
|
||||
send a more detailed response within 48 hours indicating the next steps in
|
||||
handling your report. After the initial reply to your report, the security
|
||||
team will endeavor to keep you informed of the progress towards a fix and
|
||||
full announcement, and may ask for additional information or guidance.
|
||||
|
||||
Report security vulnerabilities in third-party modules to the person or
|
||||
team maintaining the module.
|
||||
|
||||
## Disclosure Policy
|
||||
|
||||
When the security team receives a security bug report, they will assign it
|
||||
to a primary handler. This person will coordinate the fix and release
|
||||
process, involving the following steps:
|
||||
|
||||
* Confirm the problem.
|
||||
* Audit code to find any potential similar problems.
|
||||
* Prepare fixes and release them as fast as possible.
|
||||
-127
@@ -1,127 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import { renderError } from "../src/common/render.js";
|
||||
import { isLocaleAvailable } from "../src/translations.js";
|
||||
import { renderGistCard } from "../src/cards/gist.js";
|
||||
import { fetchGist } from "../src/fetchers/gist.js";
|
||||
import {
|
||||
CACHE_TTL,
|
||||
resolveCacheSeconds,
|
||||
setCacheHeaders,
|
||||
setErrorCacheHeaders,
|
||||
} from "../src/common/cache.js";
|
||||
import { guardAccess } from "../src/common/access.js";
|
||||
import {
|
||||
MissingParamError,
|
||||
retrieveSecondaryMessage,
|
||||
} from "../src/common/error.js";
|
||||
import { parseBoolean } from "../src/common/ops.js";
|
||||
|
||||
// @ts-ignore
|
||||
export default async (req, res) => {
|
||||
const {
|
||||
id,
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
cache_seconds,
|
||||
locale,
|
||||
border_radius,
|
||||
border_color,
|
||||
show_owner,
|
||||
hide_border,
|
||||
} = req.query;
|
||||
|
||||
res.setHeader("Content-Type", "image/svg+xml");
|
||||
|
||||
const access = guardAccess({
|
||||
res,
|
||||
id,
|
||||
type: "gist",
|
||||
colors: {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
},
|
||||
});
|
||||
if (!access.isPassed) {
|
||||
return access.result;
|
||||
}
|
||||
|
||||
if (locale && !isLocaleAvailable(locale)) {
|
||||
return res.send(
|
||||
renderError({
|
||||
message: "Something went wrong",
|
||||
secondaryMessage: "Language not found",
|
||||
renderOptions: {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const gistData = await fetchGist(id);
|
||||
const cacheSeconds = resolveCacheSeconds({
|
||||
requested: parseInt(cache_seconds, 10),
|
||||
def: CACHE_TTL.GIST_CARD.DEFAULT,
|
||||
min: CACHE_TTL.GIST_CARD.MIN,
|
||||
max: CACHE_TTL.GIST_CARD.MAX,
|
||||
});
|
||||
|
||||
setCacheHeaders(res, cacheSeconds);
|
||||
|
||||
return res.send(
|
||||
renderGistCard(gistData, {
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
border_radius,
|
||||
border_color,
|
||||
locale: locale ? locale.toLowerCase() : null,
|
||||
show_owner: parseBoolean(show_owner),
|
||||
hide_border: parseBoolean(hide_border),
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
setErrorCacheHeaders(res);
|
||||
if (err instanceof Error) {
|
||||
return res.send(
|
||||
renderError({
|
||||
message: err.message,
|
||||
secondaryMessage: retrieveSecondaryMessage(err),
|
||||
renderOptions: {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
show_repo_link: !(err instanceof MissingParamError),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
return res.send(
|
||||
renderError({
|
||||
message: "An unknown error occurred",
|
||||
renderOptions: {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
+48
-140
@@ -1,167 +1,75 @@
|
||||
// @ts-check
|
||||
require("dotenv").config();
|
||||
const {
|
||||
renderError,
|
||||
parseBoolean,
|
||||
parseArray,
|
||||
clampValue,
|
||||
CONSTANTS,
|
||||
} = require("../src/common/utils");
|
||||
const fetchStats = require("../src/fetchers/stats-fetcher");
|
||||
const renderStatsCard = require("../src/cards/stats-card");
|
||||
|
||||
import { renderStatsCard } from "../src/cards/stats.js";
|
||||
import { guardAccess } from "../src/common/access.js";
|
||||
import {
|
||||
CACHE_TTL,
|
||||
resolveCacheSeconds,
|
||||
setCacheHeaders,
|
||||
setErrorCacheHeaders,
|
||||
} from "../src/common/cache.js";
|
||||
import {
|
||||
MissingParamError,
|
||||
retrieveSecondaryMessage,
|
||||
} from "../src/common/error.js";
|
||||
import { parseArray, parseBoolean } from "../src/common/ops.js";
|
||||
import { renderError } from "../src/common/render.js";
|
||||
import { fetchStats } from "../src/fetchers/stats.js";
|
||||
import { isLocaleAvailable } from "../src/translations.js";
|
||||
|
||||
// @ts-ignore
|
||||
export default async (req, res) => {
|
||||
module.exports = async (req, res) => {
|
||||
const {
|
||||
username,
|
||||
hide,
|
||||
show_stats,
|
||||
hide_title,
|
||||
hide_border,
|
||||
card_width,
|
||||
hide_rank,
|
||||
show_icons,
|
||||
count_private,
|
||||
include_all_commits,
|
||||
commits_year,
|
||||
line_height,
|
||||
title_color,
|
||||
ring_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
text_bold,
|
||||
bg_color,
|
||||
theme,
|
||||
cache_seconds,
|
||||
exclude_repo,
|
||||
custom_title,
|
||||
locale,
|
||||
disable_animations,
|
||||
border_radius,
|
||||
number_format,
|
||||
number_precision,
|
||||
border_color,
|
||||
rank_icon,
|
||||
show,
|
||||
} = req.query;
|
||||
let stats;
|
||||
|
||||
res.setHeader("Content-Type", "image/svg+xml");
|
||||
|
||||
const access = guardAccess({
|
||||
res,
|
||||
id: username,
|
||||
type: "username",
|
||||
colors: {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
},
|
||||
});
|
||||
if (!access.isPassed) {
|
||||
return access.result;
|
||||
}
|
||||
|
||||
if (locale && !isLocaleAvailable(locale)) {
|
||||
return res.send(
|
||||
renderError({
|
||||
message: "Something went wrong",
|
||||
secondaryMessage: "Language not found",
|
||||
renderOptions: {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const showStats = parseArray(show);
|
||||
const stats = await fetchStats(
|
||||
stats = await fetchStats(
|
||||
username,
|
||||
parseBoolean(include_all_commits),
|
||||
parseArray(exclude_repo),
|
||||
showStats.includes("prs_merged") ||
|
||||
showStats.includes("prs_merged_percentage"),
|
||||
showStats.includes("discussions_started"),
|
||||
showStats.includes("discussions_answered"),
|
||||
parseInt(commits_year, 10),
|
||||
);
|
||||
const cacheSeconds = resolveCacheSeconds({
|
||||
requested: parseInt(cache_seconds, 10),
|
||||
def: CACHE_TTL.STATS_CARD.DEFAULT,
|
||||
min: CACHE_TTL.STATS_CARD.MIN,
|
||||
max: CACHE_TTL.STATS_CARD.MAX,
|
||||
});
|
||||
|
||||
setCacheHeaders(res, cacheSeconds);
|
||||
|
||||
return res.send(
|
||||
renderStatsCard(stats, {
|
||||
hide: parseArray(hide),
|
||||
show_icons: parseBoolean(show_icons),
|
||||
hide_title: parseBoolean(hide_title),
|
||||
hide_border: parseBoolean(hide_border),
|
||||
card_width: parseInt(card_width, 10),
|
||||
hide_rank: parseBoolean(hide_rank),
|
||||
include_all_commits: parseBoolean(include_all_commits),
|
||||
commits_year: parseInt(commits_year, 10),
|
||||
line_height,
|
||||
title_color,
|
||||
ring_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
text_bold: parseBoolean(text_bold),
|
||||
bg_color,
|
||||
theme,
|
||||
custom_title,
|
||||
border_radius,
|
||||
border_color,
|
||||
number_format,
|
||||
number_precision: parseInt(number_precision, 10),
|
||||
locale: locale ? locale.toLowerCase() : null,
|
||||
disable_animations: parseBoolean(disable_animations),
|
||||
rank_icon,
|
||||
show: showStats,
|
||||
}),
|
||||
parseBoolean(count_private),
|
||||
parseBoolean(include_all_commits)
|
||||
);
|
||||
} catch (err) {
|
||||
setErrorCacheHeaders(res);
|
||||
if (err instanceof Error) {
|
||||
return res.send(
|
||||
renderError({
|
||||
message: err.message,
|
||||
secondaryMessage: retrieveSecondaryMessage(err),
|
||||
renderOptions: {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
show_repo_link: !(err instanceof MissingParamError),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
return res.send(
|
||||
renderError({
|
||||
message: "An unknown error occurred",
|
||||
renderOptions: {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
},
|
||||
}),
|
||||
renderError(
|
||||
err.message,
|
||||
"Make sure the provided username is not an organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const cacheSeconds = clampValue(
|
||||
parseInt(cache_seconds || CONSTANTS.THIRTY_MINUTES, 10),
|
||||
CONSTANTS.THIRTY_MINUTES,
|
||||
CONSTANTS.ONE_DAY
|
||||
);
|
||||
|
||||
res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`);
|
||||
|
||||
res.send(
|
||||
renderStatsCard(stats, {
|
||||
hide: parseArray(hide),
|
||||
show_stats: parseArray(show_stats),
|
||||
show_icons: parseBoolean(show_icons),
|
||||
hide_title: parseBoolean(hide_title),
|
||||
hide_border: parseBoolean(hide_border),
|
||||
hide_rank: parseBoolean(hide_rank),
|
||||
include_all_commits: parseBoolean(include_all_commits),
|
||||
line_height,
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
+47
-110
@@ -1,28 +1,18 @@
|
||||
// @ts-check
|
||||
require("dotenv").config();
|
||||
const {
|
||||
renderError,
|
||||
parseBoolean,
|
||||
clampValue,
|
||||
CONSTANTS,
|
||||
logger,
|
||||
} = require("../src/common/utils");
|
||||
const fetchRepo = require("../src/fetchers/repo-fetcher");
|
||||
const renderRepoCard = require("../src/cards/repo-card");
|
||||
|
||||
import { renderRepoCard } from "../src/cards/repo.js";
|
||||
import { guardAccess } from "../src/common/access.js";
|
||||
import {
|
||||
CACHE_TTL,
|
||||
resolveCacheSeconds,
|
||||
setCacheHeaders,
|
||||
setErrorCacheHeaders,
|
||||
} from "../src/common/cache.js";
|
||||
import {
|
||||
MissingParamError,
|
||||
retrieveSecondaryMessage,
|
||||
} from "../src/common/error.js";
|
||||
import { parseBoolean } from "../src/common/ops.js";
|
||||
import { renderError } from "../src/common/render.js";
|
||||
import { fetchRepo } from "../src/fetchers/repo.js";
|
||||
import { isLocaleAvailable } from "../src/translations.js";
|
||||
|
||||
// @ts-ignore
|
||||
export default async (req, res) => {
|
||||
module.exports = async (req, res) => {
|
||||
const {
|
||||
username,
|
||||
repo,
|
||||
hide_border,
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
@@ -30,101 +20,48 @@ export default async (req, res) => {
|
||||
theme,
|
||||
show_owner,
|
||||
cache_seconds,
|
||||
locale,
|
||||
border_radius,
|
||||
border_color,
|
||||
description_lines_count,
|
||||
} = req.query;
|
||||
|
||||
let repoData;
|
||||
|
||||
res.setHeader("Content-Type", "image/svg+xml");
|
||||
|
||||
const access = guardAccess({
|
||||
res,
|
||||
id: username,
|
||||
type: "username",
|
||||
colors: {
|
||||
try {
|
||||
repoData = await fetchRepo(username, repo);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return res.send(renderError(err.message));
|
||||
}
|
||||
|
||||
let cacheSeconds = clampValue(
|
||||
parseInt(cache_seconds || CONSTANTS.THIRTY_MINUTES, 10),
|
||||
CONSTANTS.THIRTY_MINUTES,
|
||||
CONSTANTS.ONE_DAY
|
||||
);
|
||||
|
||||
/*
|
||||
if star count & fork count is over 1k then we are kFormating the text
|
||||
and if both are zero we are not showing the stats
|
||||
so we can just make the cache longer, since there is no need to frequent updates
|
||||
*/
|
||||
const stars = repoData.stargazers.totalCount;
|
||||
const forks = repoData.forkCount;
|
||||
const isBothOver1K = stars > 1000 && forks > 1000;
|
||||
const isBothUnder1 = stars < 1 && forks < 1;
|
||||
if (!cache_seconds && (isBothOver1K || isBothUnder1)) {
|
||||
cacheSeconds = CONSTANTS.TWO_HOURS;
|
||||
}
|
||||
|
||||
res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`);
|
||||
|
||||
res.send(
|
||||
renderRepoCard(repoData, {
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
},
|
||||
});
|
||||
if (!access.isPassed) {
|
||||
return access.result;
|
||||
}
|
||||
|
||||
if (locale && !isLocaleAvailable(locale)) {
|
||||
return res.send(
|
||||
renderError({
|
||||
message: "Something went wrong",
|
||||
secondaryMessage: "Language not found",
|
||||
renderOptions: {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const repoData = await fetchRepo(username, repo);
|
||||
const cacheSeconds = resolveCacheSeconds({
|
||||
requested: parseInt(cache_seconds, 10),
|
||||
def: CACHE_TTL.PIN_CARD.DEFAULT,
|
||||
min: CACHE_TTL.PIN_CARD.MIN,
|
||||
max: CACHE_TTL.PIN_CARD.MAX,
|
||||
});
|
||||
|
||||
setCacheHeaders(res, cacheSeconds);
|
||||
|
||||
return res.send(
|
||||
renderRepoCard(repoData, {
|
||||
hide_border: parseBoolean(hide_border),
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
border_radius,
|
||||
border_color,
|
||||
show_owner: parseBoolean(show_owner),
|
||||
locale: locale ? locale.toLowerCase() : null,
|
||||
description_lines_count,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
setErrorCacheHeaders(res);
|
||||
if (err instanceof Error) {
|
||||
return res.send(
|
||||
renderError({
|
||||
message: err.message,
|
||||
secondaryMessage: retrieveSecondaryMessage(err),
|
||||
renderOptions: {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
show_repo_link: !(err instanceof MissingParamError),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
return res.send(
|
||||
renderError({
|
||||
message: "An unknown error occurred",
|
||||
renderOptions: {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
show_owner: parseBoolean(show_owner),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @file Contains a simple cloud function that can be used to check which PATs are no
|
||||
* longer working. It returns a list of valid PATs, expired PATs and PATs with errors.
|
||||
*
|
||||
* @description This function is currently rate limited to 1 request per 5 minutes.
|
||||
*/
|
||||
|
||||
import { request } from "../../src/common/http.js";
|
||||
import { logger } from "../../src/common/log.js";
|
||||
import { dateDiff } from "../../src/common/ops.js";
|
||||
|
||||
export const RATE_LIMIT_SECONDS = 60 * 5; // 1 request per 5 minutes
|
||||
|
||||
/**
|
||||
* Simple uptime check fetcher for the PATs.
|
||||
*
|
||||
* @param {any} variables Fetcher variables.
|
||||
* @param {string} token GitHub token.
|
||||
* @returns {Promise<import('axios').AxiosResponse>} The response.
|
||||
*/
|
||||
const uptimeFetcher = (variables, token) => {
|
||||
return request(
|
||||
{
|
||||
query: `
|
||||
query {
|
||||
rateLimit {
|
||||
remaining
|
||||
resetAt
|
||||
},
|
||||
}`,
|
||||
variables,
|
||||
},
|
||||
{
|
||||
Authorization: `bearer ${token}`,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const getAllPATs = () => {
|
||||
return Object.keys(process.env).filter((key) => /PAT_\d*$/.exec(key));
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {(variables: any, token: string) => Promise<import('axios').AxiosResponse>} Fetcher The fetcher function.
|
||||
* @typedef {{validPATs: string[], expiredPATs: string[], exhaustedPATs: string[], suspendedPATs: string[], errorPATs: string[], details: any}} PATInfo The PAT info.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check whether any of the PATs is expired.
|
||||
*
|
||||
* @param {Fetcher} fetcher The fetcher function.
|
||||
* @param {any} variables Fetcher variables.
|
||||
* @returns {Promise<PATInfo>} The response.
|
||||
*/
|
||||
const getPATInfo = async (fetcher, variables) => {
|
||||
/** @type {Record<string, any>} */
|
||||
const details = {};
|
||||
const PATs = getAllPATs();
|
||||
|
||||
for (const pat of PATs) {
|
||||
try {
|
||||
const response = await fetcher(variables, process.env[pat]);
|
||||
const errors = response.data.errors;
|
||||
const hasErrors = Boolean(errors);
|
||||
const errorType = errors?.[0]?.type;
|
||||
const isRateLimited =
|
||||
(hasErrors && errorType === "RATE_LIMITED") ||
|
||||
response.data.data?.rateLimit?.remaining === 0;
|
||||
|
||||
// Store PATs with errors.
|
||||
if (hasErrors && errorType !== "RATE_LIMITED") {
|
||||
details[pat] = {
|
||||
status: "error",
|
||||
error: {
|
||||
type: errors[0].type,
|
||||
message: errors[0].message,
|
||||
},
|
||||
};
|
||||
continue;
|
||||
} else if (isRateLimited) {
|
||||
const date1 = new Date();
|
||||
const date2 = new Date(response.data?.data?.rateLimit?.resetAt);
|
||||
details[pat] = {
|
||||
status: "exhausted",
|
||||
remaining: 0,
|
||||
resetIn: dateDiff(date2, date1) + " minutes",
|
||||
};
|
||||
} else {
|
||||
details[pat] = {
|
||||
status: "valid",
|
||||
remaining: response.data.data.rateLimit.remaining,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
// Store the PAT if it is expired.
|
||||
const errorMessage = err.response?.data?.message?.toLowerCase();
|
||||
if (errorMessage === "bad credentials") {
|
||||
details[pat] = {
|
||||
status: "expired",
|
||||
};
|
||||
} else if (errorMessage === "sorry. your account was suspended.") {
|
||||
details[pat] = {
|
||||
status: "suspended",
|
||||
};
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const filterPATsByStatus = (status) => {
|
||||
return Object.keys(details).filter((pat) => details[pat].status === status);
|
||||
};
|
||||
|
||||
const sortedDetails = Object.keys(details)
|
||||
.sort()
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = details[key];
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
validPATs: filterPATsByStatus("valid"),
|
||||
expiredPATs: filterPATsByStatus("expired"),
|
||||
exhaustedPATs: filterPATsByStatus("exhausted"),
|
||||
suspendedPATs: filterPATsByStatus("suspended"),
|
||||
errorPATs: filterPATsByStatus("error"),
|
||||
details: sortedDetails,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Cloud function that returns information about the used PATs.
|
||||
*
|
||||
* @param {any} _ The request.
|
||||
* @param {any} res The response.
|
||||
* @returns {Promise<void>} The response.
|
||||
*/
|
||||
export default async (_, res) => {
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
try {
|
||||
// Add header to prevent abuse.
|
||||
const PATsInfo = await getPATInfo(uptimeFetcher, {});
|
||||
if (PATsInfo) {
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
`max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`,
|
||||
);
|
||||
}
|
||||
res.send(JSON.stringify(PATsInfo, null, 2));
|
||||
} catch (err) {
|
||||
// Throw error if something went wrong.
|
||||
logger.error(err);
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
res.send("Something went wrong: " + err.message);
|
||||
}
|
||||
};
|
||||
@@ -1,124 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @file Contains a simple cloud function that can be used to check if the PATs are still
|
||||
* functional.
|
||||
*
|
||||
* @description This function is currently rate limited to 1 request per 5 minutes.
|
||||
*/
|
||||
|
||||
import { request } from "../../src/common/http.js";
|
||||
import retryer from "../../src/common/retryer.js";
|
||||
import { logger } from "../../src/common/log.js";
|
||||
|
||||
export const RATE_LIMIT_SECONDS = 60 * 5; // 1 request per 5 minutes
|
||||
|
||||
/**
|
||||
* Simple uptime check fetcher for the PATs.
|
||||
*
|
||||
* @param {any} variables Fetcher variables.
|
||||
* @param {string} token GitHub token.
|
||||
* @returns {Promise<import('axios').AxiosResponse>} The response.
|
||||
*/
|
||||
const uptimeFetcher = (variables, token) => {
|
||||
return request(
|
||||
{
|
||||
query: `
|
||||
query {
|
||||
rateLimit {
|
||||
remaining
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables,
|
||||
},
|
||||
{
|
||||
Authorization: `bearer ${token}`,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* schemaVersion: number;
|
||||
* label: string;
|
||||
* message: "up" | "down";
|
||||
* color: "brightgreen" | "red";
|
||||
* isError: boolean
|
||||
* }} ShieldsResponse Shields.io response object.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates Json response that can be used for shields.io dynamic card generation.
|
||||
*
|
||||
* @param {boolean} up Whether the PATs are up or not.
|
||||
* @returns {ShieldsResponse} Dynamic shields.io JSON response object.
|
||||
*
|
||||
* @see https://shields.io/endpoint.
|
||||
*/
|
||||
const shieldsUptimeBadge = (up) => {
|
||||
const schemaVersion = 1;
|
||||
const isError = true;
|
||||
const label = "Public Instance";
|
||||
const message = up ? "up" : "down";
|
||||
const color = up ? "brightgreen" : "red";
|
||||
return {
|
||||
schemaVersion,
|
||||
label,
|
||||
message,
|
||||
color,
|
||||
isError,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Cloud function that returns whether the PATs are still functional.
|
||||
*
|
||||
* @param {any} req The request.
|
||||
* @param {any} res The response.
|
||||
* @returns {Promise<void>} Nothing.
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
let { type } = req.query;
|
||||
type = type ? type.toLowerCase() : "boolean";
|
||||
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
|
||||
try {
|
||||
let PATsValid = true;
|
||||
try {
|
||||
await retryer(uptimeFetcher, {});
|
||||
} catch (err) {
|
||||
// Resolve eslint no-unused-vars
|
||||
err;
|
||||
|
||||
PATsValid = false;
|
||||
}
|
||||
|
||||
if (PATsValid) {
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
`max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`,
|
||||
);
|
||||
} else {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case "shields":
|
||||
res.send(shieldsUptimeBadge(PATsValid));
|
||||
break;
|
||||
case "json":
|
||||
res.send({ up: PATsValid });
|
||||
break;
|
||||
default:
|
||||
res.send(PATsValid);
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
// Return fail boolean if something went wrong.
|
||||
logger.error(err);
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
res.send("Something went wrong: " + err.message);
|
||||
}
|
||||
};
|
||||
+36
-167
@@ -1,24 +1,15 @@
|
||||
// @ts-check
|
||||
require("dotenv").config();
|
||||
const {
|
||||
renderError,
|
||||
clampValue,
|
||||
parseBoolean,
|
||||
parseArray,
|
||||
CONSTANTS,
|
||||
} = require("../src/common/utils");
|
||||
const fetchTopLanguages = require("../src/fetchers/top-languages-fetcher");
|
||||
const renderTopLanguages = require("../src/cards/top-languages-card");
|
||||
|
||||
import { renderTopLanguages } from "../src/cards/top-languages.js";
|
||||
import { guardAccess } from "../src/common/access.js";
|
||||
import {
|
||||
CACHE_TTL,
|
||||
resolveCacheSeconds,
|
||||
setCacheHeaders,
|
||||
setErrorCacheHeaders,
|
||||
} from "../src/common/cache.js";
|
||||
import {
|
||||
MissingParamError,
|
||||
retrieveSecondaryMessage,
|
||||
} from "../src/common/error.js";
|
||||
import { parseArray, parseBoolean } from "../src/common/ops.js";
|
||||
import { renderError } from "../src/common/render.js";
|
||||
import { fetchTopLanguages } from "../src/fetchers/top-languages.js";
|
||||
import { isLocaleAvailable } from "../src/translations.js";
|
||||
|
||||
// @ts-ignore
|
||||
export default async (req, res) => {
|
||||
module.exports = async (req, res) => {
|
||||
const {
|
||||
username,
|
||||
hide,
|
||||
@@ -31,158 +22,36 @@ export default async (req, res) => {
|
||||
theme,
|
||||
cache_seconds,
|
||||
layout,
|
||||
langs_count,
|
||||
exclude_repo,
|
||||
size_weight,
|
||||
count_weight,
|
||||
custom_title,
|
||||
locale,
|
||||
border_radius,
|
||||
border_color,
|
||||
disable_animations,
|
||||
hide_progress,
|
||||
stats_format,
|
||||
} = req.query;
|
||||
let topLangs;
|
||||
|
||||
res.setHeader("Content-Type", "image/svg+xml");
|
||||
|
||||
const access = guardAccess({
|
||||
res,
|
||||
id: username,
|
||||
type: "username",
|
||||
colors: {
|
||||
try {
|
||||
topLangs = await fetchTopLanguages(username);
|
||||
} catch (err) {
|
||||
return res.send(renderError(err.message));
|
||||
}
|
||||
|
||||
const cacheSeconds = clampValue(
|
||||
parseInt(cache_seconds || CONSTANTS.THIRTY_MINUTES, 10),
|
||||
CONSTANTS.THIRTY_MINUTES,
|
||||
CONSTANTS.ONE_DAY
|
||||
);
|
||||
|
||||
res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`);
|
||||
|
||||
res.send(
|
||||
renderTopLanguages(topLangs, {
|
||||
hide_title: parseBoolean(hide_title),
|
||||
hide_border: parseBoolean(hide_border),
|
||||
card_width: parseInt(card_width, 10),
|
||||
hide: parseArray(hide),
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
},
|
||||
});
|
||||
if (!access.isPassed) {
|
||||
return access.result;
|
||||
}
|
||||
|
||||
if (locale && !isLocaleAvailable(locale)) {
|
||||
return res.send(
|
||||
renderError({
|
||||
message: "Something went wrong",
|
||||
secondaryMessage: "Locale not found",
|
||||
renderOptions: {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
layout !== undefined &&
|
||||
(typeof layout !== "string" ||
|
||||
!["compact", "normal", "donut", "donut-vertical", "pie"].includes(layout))
|
||||
) {
|
||||
return res.send(
|
||||
renderError({
|
||||
message: "Something went wrong",
|
||||
secondaryMessage: "Incorrect layout input",
|
||||
renderOptions: {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
stats_format !== undefined &&
|
||||
(typeof stats_format !== "string" ||
|
||||
!["bytes", "percentages"].includes(stats_format))
|
||||
) {
|
||||
return res.send(
|
||||
renderError({
|
||||
message: "Something went wrong",
|
||||
secondaryMessage: "Incorrect stats_format input",
|
||||
renderOptions: {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const topLangs = await fetchTopLanguages(
|
||||
username,
|
||||
parseArray(exclude_repo),
|
||||
size_weight,
|
||||
count_weight,
|
||||
);
|
||||
const cacheSeconds = resolveCacheSeconds({
|
||||
requested: parseInt(cache_seconds, 10),
|
||||
def: CACHE_TTL.TOP_LANGS_CARD.DEFAULT,
|
||||
min: CACHE_TTL.TOP_LANGS_CARD.MIN,
|
||||
max: CACHE_TTL.TOP_LANGS_CARD.MAX,
|
||||
});
|
||||
|
||||
setCacheHeaders(res, cacheSeconds);
|
||||
|
||||
return res.send(
|
||||
renderTopLanguages(topLangs, {
|
||||
custom_title,
|
||||
hide_title: parseBoolean(hide_title),
|
||||
hide_border: parseBoolean(hide_border),
|
||||
card_width: parseInt(card_width, 10),
|
||||
hide: parseArray(hide),
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
layout,
|
||||
langs_count,
|
||||
border_radius,
|
||||
border_color,
|
||||
locale: locale ? locale.toLowerCase() : null,
|
||||
disable_animations: parseBoolean(disable_animations),
|
||||
hide_progress: parseBoolean(hide_progress),
|
||||
stats_format,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
setErrorCacheHeaders(res);
|
||||
if (err instanceof Error) {
|
||||
return res.send(
|
||||
renderError({
|
||||
message: err.message,
|
||||
secondaryMessage: retrieveSecondaryMessage(err),
|
||||
renderOptions: {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
show_repo_link: !(err instanceof MissingParamError),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
return res.send(
|
||||
renderError({
|
||||
message: "An unknown error occurred",
|
||||
renderOptions: {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
layout,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
-146
@@ -1,146 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import { renderWakatimeCard } from "../src/cards/wakatime.js";
|
||||
import { renderError } from "../src/common/render.js";
|
||||
import { fetchWakatimeStats } from "../src/fetchers/wakatime.js";
|
||||
import { isLocaleAvailable } from "../src/translations.js";
|
||||
import {
|
||||
CACHE_TTL,
|
||||
resolveCacheSeconds,
|
||||
setCacheHeaders,
|
||||
setErrorCacheHeaders,
|
||||
} from "../src/common/cache.js";
|
||||
import { guardAccess } from "../src/common/access.js";
|
||||
import {
|
||||
MissingParamError,
|
||||
retrieveSecondaryMessage,
|
||||
} from "../src/common/error.js";
|
||||
import { parseArray, parseBoolean } from "../src/common/ops.js";
|
||||
|
||||
// @ts-ignore
|
||||
export default async (req, res) => {
|
||||
const {
|
||||
username,
|
||||
title_color,
|
||||
icon_color,
|
||||
hide_border,
|
||||
card_width,
|
||||
line_height,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
cache_seconds,
|
||||
hide_title,
|
||||
hide_progress,
|
||||
custom_title,
|
||||
locale,
|
||||
layout,
|
||||
langs_count,
|
||||
hide,
|
||||
api_domain,
|
||||
border_radius,
|
||||
border_color,
|
||||
display_format,
|
||||
disable_animations,
|
||||
} = req.query;
|
||||
|
||||
res.setHeader("Content-Type", "image/svg+xml");
|
||||
|
||||
const access = guardAccess({
|
||||
res,
|
||||
id: username,
|
||||
type: "wakatime",
|
||||
colors: {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
},
|
||||
});
|
||||
if (!access.isPassed) {
|
||||
return access.result;
|
||||
}
|
||||
|
||||
if (locale && !isLocaleAvailable(locale)) {
|
||||
return res.send(
|
||||
renderError({
|
||||
message: "Something went wrong",
|
||||
secondaryMessage: "Language not found",
|
||||
renderOptions: {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await fetchWakatimeStats({ username, api_domain });
|
||||
const cacheSeconds = resolveCacheSeconds({
|
||||
requested: parseInt(cache_seconds, 10),
|
||||
def: CACHE_TTL.WAKATIME_CARD.DEFAULT,
|
||||
min: CACHE_TTL.WAKATIME_CARD.MIN,
|
||||
max: CACHE_TTL.WAKATIME_CARD.MAX,
|
||||
});
|
||||
|
||||
setCacheHeaders(res, cacheSeconds);
|
||||
|
||||
return res.send(
|
||||
renderWakatimeCard(stats, {
|
||||
custom_title,
|
||||
hide_title: parseBoolean(hide_title),
|
||||
hide_border: parseBoolean(hide_border),
|
||||
card_width: parseInt(card_width, 10),
|
||||
hide: parseArray(hide),
|
||||
line_height,
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
hide_progress,
|
||||
border_radius,
|
||||
border_color,
|
||||
locale: locale ? locale.toLowerCase() : null,
|
||||
layout,
|
||||
langs_count,
|
||||
display_format,
|
||||
disable_animations: parseBoolean(disable_animations),
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
setErrorCacheHeaders(res);
|
||||
if (err instanceof Error) {
|
||||
return res.send(
|
||||
renderError({
|
||||
message: err.message,
|
||||
secondaryMessage: retrieveSecondaryMessage(err),
|
||||
renderOptions: {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
show_repo_link: !(err instanceof MissingParamError),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
return res.send(
|
||||
renderError({
|
||||
message: "An unknown error occurred",
|
||||
renderOptions: {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
networks:
|
||||
public:
|
||||
external: true
|
||||
|
||||
services:
|
||||
readme-stats:
|
||||
build: .
|
||||
container_name: readme-stats
|
||||
env_file:
|
||||
- .env
|
||||
expose:
|
||||
- ${port}
|
||||
healthcheck:
|
||||
test: wget --no-verbose -O - --tries=1 http://localhost:$$port/?username=$$GH_UN | grep 'GitHub Stats' || exit 1
|
||||
interval: 60m # hourly
|
||||
retries: 3
|
||||
start_interval: 1s
|
||||
start_period: 10s
|
||||
timeout: 5s
|
||||
init: true
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=false" # built, disable watchtower
|
||||
- homepage.group=Sites
|
||||
- homepage.name=Github Readme Stats
|
||||
- homepage.icon=github
|
||||
- homepage.href=https://readme.digitaladapt.com
|
||||
- kuma.__docker
|
||||
logging:
|
||||
driver: syslog
|
||||
options:
|
||||
tag: readme_stats
|
||||
networks:
|
||||
- public
|
||||
restart: always
|
||||
user: ${USER_ID}:${GROUP_ID}
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro # read-only
|
||||
|
||||
# allow watchtower to monitor for update to base image, since our container is built
|
||||
readme_stats_monitor:
|
||||
container_name: readme_stats_monitor
|
||||
entrypoint: "/bin/true"
|
||||
image: node:lts-alpine
|
||||
restart: no
|
||||
user: ${USER_ID}:${GROUP_ID}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
services:
|
||||
readme_stats:
|
||||
build: .
|
||||
container_name: readme_stats
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
expose:
|
||||
- $port
|
||||
healthcheck:
|
||||
test: wget --no-verbose -O - --tries=1 http://localhost:$$port/?username=$$GH_UN | grep 'GitHub Stats' || exit 1
|
||||
interval: 60m # hourly
|
||||
retries: 3
|
||||
start_interval: 1s
|
||||
start_period: 10s
|
||||
timeout: 5s
|
||||
init: true
|
||||
|
||||
caddy:
|
||||
image: caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "443:443/udp"
|
||||
depends_on:
|
||||
- readme_stats
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# github token
|
||||
PAT_1=<YOUR_GITHUB_TOKEN>
|
||||
# github username
|
||||
GH_UN=<YOUR_GITHUB_USERNAME>
|
||||
# port to listen on
|
||||
port=9000
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import globals from "globals";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import jsdoc from "eslint-plugin-jsdoc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default [
|
||||
...compat.extends("prettier"),
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.browser,
|
||||
},
|
||||
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: {
|
||||
jsdoc,
|
||||
},
|
||||
rules: {
|
||||
"no-unexpected-multiline": "error",
|
||||
"accessor-pairs": [
|
||||
"error",
|
||||
{
|
||||
getWithoutSet: false,
|
||||
setWithoutGet: true,
|
||||
},
|
||||
],
|
||||
"block-scoped-var": "warn",
|
||||
"consistent-return": "error",
|
||||
curly: "error",
|
||||
"no-alert": "error",
|
||||
"no-caller": "error",
|
||||
"no-warning-comments": [
|
||||
"warn",
|
||||
{
|
||||
terms: ["TODO", "FIXME"],
|
||||
location: "start",
|
||||
},
|
||||
],
|
||||
"no-with": "warn",
|
||||
radix: "warn",
|
||||
"no-delete-var": "error",
|
||||
"no-undef-init": "off",
|
||||
"no-undef": "error",
|
||||
"no-undefined": "off",
|
||||
"no-unused-vars": "warn",
|
||||
"no-use-before-define": "error",
|
||||
"constructor-super": "error",
|
||||
"no-class-assign": "error",
|
||||
"no-const-assign": "error",
|
||||
"no-dupe-class-members": "error",
|
||||
"no-this-before-super": "error",
|
||||
"object-shorthand": ["warn"],
|
||||
"no-mixed-spaces-and-tabs": "warn",
|
||||
"no-multiple-empty-lines": "warn",
|
||||
"no-negated-condition": "warn",
|
||||
"no-unneeded-ternary": "warn",
|
||||
"keyword-spacing": [
|
||||
"error",
|
||||
{
|
||||
before: true,
|
||||
after: true,
|
||||
},
|
||||
],
|
||||
"jsdoc/require-returns": "warn",
|
||||
"jsdoc/require-returns-description": "warn",
|
||||
"jsdoc/require-param-description": "warn",
|
||||
"jsdoc/require-jsdoc": "warn",
|
||||
},
|
||||
},
|
||||
];
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
import "dotenv/config";
|
||||
import statsCard from "./api/index.js";
|
||||
import repoCard from "./api/pin.js";
|
||||
import langCard from "./api/top-langs.js";
|
||||
import wakatimeCard from "./api/wakatime.js";
|
||||
import gistCard from "./api/gist.js";
|
||||
import express from "express";
|
||||
|
||||
const app = express();
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/", statsCard);
|
||||
router.get("/pin", repoCard);
|
||||
router.get("/top-langs", langCard);
|
||||
router.get("/wakatime", wakatimeCard);
|
||||
router.get("/gist", gistCard);
|
||||
|
||||
app.use("/api", router);
|
||||
|
||||
const port = process.env.PORT || process.env.port || 9000;
|
||||
const server = app.listen(port, "0.0.0.0", () => {
|
||||
console.log(`Server running on port ${port}`);
|
||||
});
|
||||
|
||||
/* stop gracefully when requested */
|
||||
process.on("SIGTERM", () => {
|
||||
console.log("stopping, received SIGTERM signal");
|
||||
server.close(() => {
|
||||
process.exit();
|
||||
});
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
export default {
|
||||
clearMocks: true,
|
||||
transform: {},
|
||||
testEnvironment: "jsdom",
|
||||
coverageProvider: "v8",
|
||||
testPathIgnorePatterns: ["<rootDir>/node_modules/", "<rootDir>/tests/e2e/"],
|
||||
modulePathIgnorePatterns: ["<rootDir>/node_modules/", "<rootDir>/tests/e2e/"],
|
||||
coveragePathIgnorePatterns: [
|
||||
"<rootDir>/node_modules/",
|
||||
"<rootDir>/tests/e2e/",
|
||||
],
|
||||
testRegex: "(\\.bench)\\.(ts|tsx|js)$",
|
||||
};
|
||||
+1
-10
@@ -1,12 +1,3 @@
|
||||
export default {
|
||||
module.exports = {
|
||||
clearMocks: true,
|
||||
transform: {},
|
||||
testEnvironment: "jsdom",
|
||||
coverageProvider: "v8",
|
||||
testPathIgnorePatterns: ["<rootDir>/node_modules/", "<rootDir>/tests/e2e/"],
|
||||
modulePathIgnorePatterns: ["<rootDir>/node_modules/", "<rootDir>/tests/e2e/"],
|
||||
coveragePathIgnorePatterns: [
|
||||
"<rootDir>/node_modules/",
|
||||
"<rootDir>/tests/E2E/",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export default {
|
||||
clearMocks: true,
|
||||
transform: {},
|
||||
testEnvironment: "node",
|
||||
coverageProvider: "v8",
|
||||
testMatch: ["<rootDir>/tests/e2e/**/*.test.js"],
|
||||
};
|
||||
Generated
-13488
File diff suppressed because it is too large
Load Diff
+22
-61
@@ -1,76 +1,37 @@
|
||||
{
|
||||
"name": "github-readme-stats",
|
||||
"version": "1.0.0",
|
||||
"description": "Dynamically generate stats for your GitHub readme",
|
||||
"keywords": [
|
||||
"github-readme-stats",
|
||||
"readme-stats",
|
||||
"cards",
|
||||
"card-generator"
|
||||
],
|
||||
"main": "src/index.js",
|
||||
"type": "module",
|
||||
"homepage": "https://github.com/anuraghazra/github-readme-stats",
|
||||
"bugs": {
|
||||
"url": "https://github.com/anuraghazra/github-readme-stats/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/anuraghazra/github-readme-stats.git"
|
||||
},
|
||||
"description": "Dynamically generate stats for your github readmes",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
||||
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
|
||||
"test:update:snapshot": "node --experimental-vm-modules node_modules/jest/bin/jest.js -u",
|
||||
"test:e2e": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.e2e.config.js",
|
||||
"test": "jest --coverage",
|
||||
"test:watch": "jest --watch",
|
||||
"theme-readme-gen": "node scripts/generate-theme-doc",
|
||||
"preview-theme": "node scripts/preview-theme",
|
||||
"close-stale-theme-prs": "node scripts/close-stale-theme-prs",
|
||||
"generate-langs-json": "node scripts/generate-langs-json",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"prepare": "husky",
|
||||
"lint": "npx eslint --max-warnings 0 \"./src/**/*.js\" \"./scripts/**/*.js\" \"./tests/**/*.js\" \"./api/**/*.js\" \"./themes/**/*.js\"",
|
||||
"bench": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.bench.config.js"
|
||||
"preview-theme": "node scripts/preview-theme"
|
||||
},
|
||||
"author": "Anurag Hazra",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@actions/core": "^2.0.1",
|
||||
"@actions/github": "^6.0.1",
|
||||
"@eslint/eslintrc": "^3.3.3",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@uppercod/css-to-object": "^1.1.1",
|
||||
"axios-mock-adapter": "^2.1.0",
|
||||
"color-contrast-checker": "^2.1.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-jsdoc": "^61.5.0",
|
||||
"express": "^5.2.1",
|
||||
"globals": "^16.5.0",
|
||||
"hjson": "^3.2.2",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^30.2.0",
|
||||
"jest-environment-jsdom": "^30.2.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"lint-staged": "^16.2.7",
|
||||
"lodash.snakecase": "^4.1.1",
|
||||
"parse-diff": "^0.11.1",
|
||||
"prettier": "^3.7.3"
|
||||
"@actions/core": "^1.2.4",
|
||||
"@actions/github": "^4.0.0",
|
||||
"@testing-library/dom": "^7.20.0",
|
||||
"@testing-library/jest-dom": "^5.11.0",
|
||||
"axios": "^0.19.2",
|
||||
"axios-mock-adapter": "^1.18.1",
|
||||
"css-to-object": "^1.1.0",
|
||||
"husky": "^4.2.5",
|
||||
"jest": "^26.1.0",
|
||||
"parse-diff": "^0.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"emoji-name-map": "^2.0.3",
|
||||
"dotenv": "^8.2.0",
|
||||
"emoji-name-map": "^1.2.8",
|
||||
"github-username-regex": "^1.0.0",
|
||||
"word-wrap": "^1.2.5"
|
||||
"word-wrap": "^1.2.3"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,css,md}": "prettier --write"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -1,35 +0,0 @@
|
||||
Get dynamically generated GitHub stats on your READMEs, with the ease of docker!
|
||||
|
||||
Fork of [https://github.com/anuraghazra/github-readme-stats](https://github.com/anuraghazra/github-readme-stats), gently wrapped in docker.
|
||||
|
||||
**Steps to setup:**
|
||||
* clone this repo into a folder.
|
||||
* copy docker-compose.yml.example to docker-compose.yml.
|
||||
* copy env.example to .env and update.
|
||||
* copy Caddyfile.example to Caddyfile and update.
|
||||
* and start with `docker compose up -d`.
|
||||
|
||||
docker-compose.yml.example has a very simple Caddy reverse proxy, for automatic https.
|
||||
|
||||
Files have example in their name, so that when you update, your local settings won't be overridden.
|
||||
|
||||
```
|
||||
git clone https://code.digitaladapt.com/andrew/github-readme-stats.git
|
||||
cd github-readme-stats
|
||||
cp docker-compose.yml.example docker-compose.yml
|
||||
cp env.example .env
|
||||
vim .env
|
||||
# add your github token
|
||||
|
||||
cp Caddyfile.example Caddyfile
|
||||
vim Caddyfile
|
||||
# configure your domain
|
||||
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
You might ask, why should I trust your docker image?
|
||||
|
||||
The answer is, you don't have to, there isn't an image,
|
||||
just a few commands in the Dockerfile, which you can and should look at.
|
||||
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
<p align="center">
|
||||
<img width="100px" src="https://res.cloudinary.com/anuraghazra/image/upload/v1594908242/logo_ccswme.svg" align="center" alt="GitHub Readme Stats" />
|
||||
<h2 align="center">GitHub Readme Stats</h2>
|
||||
<p align="center">在你的 README 中获取动态生成的 GitHub 统计信息!</p>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/actions">
|
||||
<img alt="Tests Passing" src="https://github.com/anuraghazra/github-readme-stats/workflows/Test/badge.svg" />
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/anuraghazra/github-readme-stats">
|
||||
<img src="https://codecov.io/gh/anuraghazra/github-readme-stats/branch/master/graph/badge.svg" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues">
|
||||
<img alt="Issues" src="https://img.shields.io/github/issues/anuraghazra/github-readme-stats?color=0088ff" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/pulls">
|
||||
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/anuraghazra/github-readme-stats?color=0088ff" />
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://a.paddle.com/v2/click/16413/119403?link=1227">
|
||||
<img src="https://img.shields.io/badge/Supported%20by-VSCode%20Power%20User%20%E2%86%92-gray.svg?colorA=655BE1&colorB=4F44D6&style=for-the-badge"/>
|
||||
</a>
|
||||
<a href="https://a.paddle.com/v2/click/16413/119403?link=2345">
|
||||
<img src="https://img.shields.io/badge/Supported%20by-Node%20Cli.com%20%E2%86%92-gray.svg?colorA=61c265&colorB=4CAF50&style=for-the-badge"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#demo">查看 Demo</a>
|
||||
·
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues/new/choose">报告 bug</a>
|
||||
·
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues/new/choose">请求增加功能</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="readme.md">English</a>
|
||||
·
|
||||
<a href="readme_es.md">Español</a>
|
||||
·
|
||||
<a href="readme_ja.md">日本語</a>
|
||||
</p>
|
||||
</p>
|
||||
<p align="center">喜欢这个项目?请考虑<a href="https://www.paypal.me/anuraghazra">捐赠</a>来帮助它完善!
|
||||
|
||||
# 特性
|
||||
|
||||
- [GitHub 统计卡片](#GitHub-统计卡片)
|
||||
- [GitHub 更多置顶](#GitHub-更多置顶)
|
||||
- [热门语言卡片](#热门语言卡片)
|
||||
- [主题](#主题)
|
||||
- [自定义](#自定义)
|
||||
- [自己部署](#自己部署)
|
||||
|
||||
# GitHub 统计卡片
|
||||
|
||||
将这行代码复制到你的 markdown 文件中,简单如此!
|
||||
|
||||
更改 `?username=` 的值为你的 GitHub 用户名。
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
_注: 排名基于用户的统计信息计算得出,详见 [src/calculateRank.js](./src/calculateRank.js)_
|
||||
|
||||
### 隐藏个别统计项
|
||||
|
||||
想要隐藏指定统计信息,你可以调用参数 `?hide=`,其值用 `,` 分隔。
|
||||
|
||||
> 选项:`&hide=stars,commits,prs,issues,contribs`
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### 把私人贡献计数添加到总提交计数中
|
||||
|
||||
你可以用参数 `?count_private=true` 把私人贡献计数添加到总提交计数中。
|
||||
|
||||
_注:如果你是自己部署本项目,私人贡献将会默认被计数,如果不是自己部署,你需要分享你的私人贡献计数。_
|
||||
|
||||
> Options: `&count_private=true`
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### 显示图标
|
||||
|
||||
想要显示图标,你可以调用 `show_icons=true` 参数,如下:
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### 主题
|
||||
|
||||
你可以通过现有的主题进行卡片个性化,省去[手动自定义](#自定义)的麻烦。
|
||||
|
||||
调用 `?theme=THEME_NAME` 参数,如下:
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
#### 所有现有主题
|
||||
|
||||
dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula
|
||||
|
||||
<img src="https://res.cloudinary.com/anuraghazra/image/upload/v1595174536/grs-themes_l4ynja.png" alt="GitHub Readme Stat Themes" width="600px"/>
|
||||
|
||||
在 [theme config 文件](./themes/index.js) 中查看更多主题,或者 **贡献新的主题** :D
|
||||
|
||||
### 自定义
|
||||
|
||||
你可以通过使用 URL 参数的方式,为你的 `Stats Card` 或 `Repo Card` 自定义样式。
|
||||
|
||||
自定义选项:
|
||||
|
||||
| Option | type | description | Stats Card (default) | Repo Card (default) | Top Lang Card (default) |
|
||||
| ------------- | --------- | ---------------------------- | -------------------- | ------------------- | ----------------------- |
|
||||
| title_color | hex color | 标题颜色 | 2f80ed | 2f80ed | 2f80ed |
|
||||
| text_color | hex color | 字体颜色 | 333 | 333 | 333 |
|
||||
| icon_color | hex color | 图标颜色 | 4c71f2 | 586069 | 586069 |
|
||||
| bg_color | hex color | 卡片背景颜色 | FFFEFE | FFFEFE | FFFEFE |
|
||||
| line_height | number | 文字行高 | 30 | N/A | N/A |
|
||||
| hide | CSV | 隐藏指定统计项 | undfined | N/A | undefined |
|
||||
| hide_rank | boolean | 隐藏评分等级 | false | N/A | N/A |
|
||||
| hide_title | boolean | 隐藏卡片标题 | false | N/A | false |
|
||||
| hide_border | boolean | 隐藏卡片边框 | false | N/A | N/A |
|
||||
| show_owner | boolean | 显示 Repo 卡片所属账户用户名 | N/A | false | N/A |
|
||||
| show_icons | boolean | 显示图标 | false | N/A | N/A |
|
||||
| theme | string | 设置主题 | 'default' | 'default_repocard' | 'default' |
|
||||
| cache_seconds | number | 手动设置自定义缓存控制 | 1800 | 1800 | 1800 |
|
||||
| count_private | boolean | 统计私人贡献计数 | false | N/A | N/A |
|
||||
| layout | string | 布局方式 | N/A | N/A | 'default' |
|
||||
|
||||
> 注意缓存:Repo 卡片默认缓存 30 分钟,如果 fork 数和 star 数小于 1k ,则默认为 2 小时。缓存被限制为最少 30 分钟,最长 24 小时。
|
||||
|
||||
# GitHub 更多置顶
|
||||
|
||||
GitHub 更多置顶 让你使用 README Profile,在个人页面中置顶多于 6 个 repo 。
|
||||
|
||||
这波可以!你再也不用受限于最多 6 个置顶了。
|
||||
|
||||
### 使用细则
|
||||
|
||||
复制粘贴这段代码到你的 README 文件中,并更改链接。
|
||||
|
||||
Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Demo
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
使用 [show_owner](#自定义) 变量将 Repo 所属账户的用户名包含在内。
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
# 热门语言卡片
|
||||
|
||||
热门语言卡片显示了 GitHub 用户常用的编程语言。
|
||||
|
||||
_注意:热门语言并不表示我的技能水平或类似的水平,它是用户在 GitHub 上拥有最多代码的一项指标,它是 github-readme-stats 的新功能_
|
||||
|
||||
### 使用细则
|
||||
|
||||
将此代码复制粘贴到您的`README.md`文件中,并改变链接。
|
||||
|
||||
Endpoint: `api/top-langs?username=anuraghazra`
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### 隐藏特定语言
|
||||
|
||||
可以使用`?hide=语言1,语言2`参数来隐藏指定的语言。
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### 紧凑的语言卡片布局
|
||||
|
||||
你可以使用 `&layout=compact` 参数来改变卡片的样式。
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Demo
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
- 紧凑布局
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
---
|
||||
|
||||
### 全部 Demo
|
||||
|
||||
- 默认
|
||||
|
||||

|
||||
|
||||
- 隐藏特定数据
|
||||
|
||||

|
||||
|
||||
- 显示图标
|
||||
|
||||

|
||||
|
||||
- 主题
|
||||
|
||||
从 [默认主题](#主题) 中进行选择
|
||||
|
||||

|
||||
|
||||
- 自定义统计卡片
|
||||
|
||||

|
||||
|
||||
- 自定义代码库卡片
|
||||
|
||||

|
||||
|
||||
- 热门语言
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
---
|
||||
|
||||
### 提示 (对齐 Repo 卡片)
|
||||
|
||||
你通常无法将图片靠边显示。为此,您可以使用以下方法:
|
||||
|
||||
```md
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats">
|
||||
<img align="left" src="https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/convoychat">
|
||||
<img align="left" src="https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=convoychat" />
|
||||
</a>
|
||||
```
|
||||
|
||||
## 自己部署
|
||||
|
||||
因为 GitHub 的 API 每个小时只允许 5 千次请求,我的 `https://github-readme-stats.vercel.app/api` 很有可能会触发限制
|
||||
如果你将其托管在自己的 Vercel 服务器上,那么你就不必为此担心。点击 deploy 按钮来开始你的部署!
|
||||
|
||||
注意: 从 [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) 开始,我们应该能够处理超过 5 千 的请求,并且不会出现宕机问题 :D
|
||||
|
||||
[](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
<details>
|
||||
<summary>设置 Vercel 的指导</summary>
|
||||
|
||||
1. 前往 [vercel.com](https://vercel.com/)
|
||||
1. 点击 `Log in`
|
||||

|
||||
1. 点击 `Continue with GitHub` 通过 GitHub 进行登录
|
||||

|
||||
1. 登录 GitHub 并允许访问所有存储库(如果系统这样提示)
|
||||
1. Fork 这个仓库
|
||||
1. 返回到你的 [Vercel dashboard](https://vercel.com/dashboard)
|
||||
1. 选择 `Import Project`
|
||||

|
||||
1. 选择 `Import Git Repository`
|
||||

|
||||
1. 选择 root 并将所有内容保持不变,并且只需添加名为 PAT_1 的环境变量(如图所示),其中将包含一个个人访问令牌(PAT),你可以在[这里](https://github.com/settings/tokens/new)轻松创建(保留默认,并且只需要命名下,名字随便)
|
||||

|
||||
1. 点击 deploy,这就完成了,查看你的域名就可使用 API 了!
|
||||
|
||||
</details>
|
||||
|
||||
## :sparkling_heart: 支持这个项目
|
||||
|
||||
我尽己所能地进行开源,并且我尽量回复每个在使用项目时需要帮助的人。很明显,这需要时间,但你可以免费享受这些。
|
||||
|
||||
然而, 如果你正在使用这个项目并感觉良好,或只是想要支持我继续开发,你可以通过如下方式:
|
||||
|
||||
- 在你的 README 中使用 github-readme-stats 时,链接指向会这里 :D
|
||||
- Star 并 分享这个项目 :rocket:
|
||||
- [](https://www.paypal.me/anuraghazra) - 你可以通过 PayPal 一次性捐款. 我多半会买一杯 ~~咖啡~~ 茶。:tea:
|
||||
|
||||
谢谢! :heart:
|
||||
|
||||
---
|
||||
|
||||
欢迎贡献! <3
|
||||
|
||||
用 :heart: 发电,用 JavaScript 制作。
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
<p align="center">
|
||||
<img width="100px" src="https://res.cloudinary.com/anuraghazra/image/upload/v1594908242/logo_ccswme.svg" align="center" alt="GitHub Readme Stats" />
|
||||
<h2 align="center">GitHub Readme Stats</h2>
|
||||
<p align="center">Zeige dynamisch generierte GitHub-Statistiken in deinen Readmes!</p>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/actions">
|
||||
<img alt="Tests Passing" src="https://github.com/anuraghazra/github-readme-stats/workflows/Test/badge.svg" />
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/anuraghazra/github-readme-stats">
|
||||
<img src="https://codecov.io/gh/anuraghazra/github-readme-stats/branch/master/graph/badge.svg" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues">
|
||||
<img alt="Issues" src="https://img.shields.io/github/issues/anuraghazra/github-readme-stats?color=0088ff" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/pulls">
|
||||
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/anuraghazra/github-readme-stats?color=0088ff" />
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://a.paddle.com/v2/click/16413/119403?link=1227">
|
||||
<img src="https://img.shields.io/badge/Supported%20by-VSCode%20Power%20User%20%E2%86%92-gray.svg?colorA=655BE1&colorB=4F44D6&style=for-the-badge"/>
|
||||
</a>
|
||||
<a href="https://a.paddle.com/v2/click/16413/119403?link=2345">
|
||||
<img src="https://img.shields.io/badge/Supported%20by-Node%20Cli.com%20%E2%86%92-gray.svg?colorA=61c265&colorB=4CAF50&style=for-the-badge"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#alle-beispiele">Beispiel ansehen</a>
|
||||
·
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues">Fehler melden</a>
|
||||
·
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues">Funktionalität anfragen</a>
|
||||
</p>
|
||||
</p>
|
||||
<p align="center">Du magst das Projekt? Wie wäre es mit einer kleinen <a href="https://www.paypal.me/anuraghazra">Spende</a> um es weiterhin am Leben zu erhalten?
|
||||
|
||||
# Funktionalitäten
|
||||
|
||||
- [GitHub Stats Card](#github-stats-card)
|
||||
- [GitHub Extra Pins](#github-extra-pins)
|
||||
- [Top Programmiersprachen Card](#top-programmiersprachen-card)
|
||||
- [Erscheinungsbild/Themes](#erscheinungsbildthemes)
|
||||
- [Anpassungen](#anpassungenpersonalisierung)
|
||||
- [Selber betreiben](#betreibe-es-auf-deiner-eigenen-vercel-instanz)
|
||||
|
||||
# GitHub Stats Card
|
||||
|
||||
Kopiere einfach folgendes in dein Markdown und das wars. Echt simpel!
|
||||
|
||||
Passe den Wert des URL-Paramters `?username=` so an, dass dort dein GitHub Username steht.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
_Hinweis: Die Berechnung des Ranges basiert auf den jeweiligen Benutzerstatistiken, siehe [src/calculateRank.js](./src/calculateRank.js)_
|
||||
|
||||
### Verbergen individueller Statistiken
|
||||
|
||||
Um eine spezifische Statistik auszublenden, kann dem Query-Parameter `?hide=` ein Array an Dingen die nicht angezeigt werden sollen übergeben werden.
|
||||
|
||||
> Optionen: `&hide=["stars","commits","prs","issues","contribs"]`
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### Icons anzeigen
|
||||
|
||||
Um Icons anzuzeigen kann der URL-Paramter `show_icons=true` wie folgt verwendet werden:
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### Erscheinungsbild/Themes
|
||||
|
||||
Mithilfe der eingebauten Themes kann das Aussehen der Karten verändern werden ohne manuelle Anpassungen vornehmen zu müssen.
|
||||
|
||||
Benutze den `?theme=THEME_NAME`-Parameter wie folgt :-
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
#### Alle eingebauten Themes :-
|
||||
|
||||
dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula
|
||||
|
||||
<img src="https://res.cloudinary.com/anuraghazra/image/upload/v1595174536/grs-themes_l4ynja.png" alt="GitHub Readme Stat Themes" width="600px"/>
|
||||
|
||||
Du kannst dir eine Vorschau [aller verfügbaren Themes](./themes/README.md) ansehen oder die [theme config Datei](./themes/index.js) auschecken.
|
||||
Außerdem **kannst du neue Themes beisteuern** wenn du möchtest, contributions sind gern gesehen :D
|
||||
|
||||
### Anpassungen/Personalisierung
|
||||
|
||||
Du kannst das Erscheinungsbild deiner `Stats Card` oder `Repo Card`, mithilfe von URL-Parametern, nach deinen Vorlieben anpassen.
|
||||
|
||||
Anpassungsoptionen:
|
||||
|
||||
| Option | Type | Beschreibung | Statistiken (default) | Repo (default) | Top Programmiersprachen (default) |
|
||||
| ---------------- | --------- | ---------------------------------------------------- | --------------------- | ------------------ | --------------------------------- |
|
||||
| title_color | hex color | Titelfarbe | 2f80ed | 2f80ed | 2f80ed |
|
||||
| text_color | hex color | Textkörperfarbe | 333 | 333 | 333 |
|
||||
| icon_color | hex color | Iconfarbe | 4c71f2 | 586069 | 586069 |
|
||||
| bg_color | hex color | Hintergrundfarbe | FFFEFE | FFFEFE | FFFEFE |
|
||||
| line_height | number | kontrolliert die Zeilenhöhe zwischen dem Text | 30 | N/A | N/A |
|
||||
| hide_rank | boolean | blendet das Ranking aus | false | N/A | N/A |
|
||||
| hide_title | boolean | blendet den Statistik-Titel aus | false | N/A | false |
|
||||
| hide_border | boolean | blendet den Rahmen aus | false | N/A | N/A |
|
||||
| show_owner | boolean | zeigt den Namen des Besitzers in der Repo-Karte | N/A | false | N/A |
|
||||
| show_icons | boolean | zeige Icons an | false | N/A | N/A |
|
||||
| theme | string | setze eingebaute themes | 'default' | 'default_repocard' | 'default' |
|
||||
| cache_seconds | number | manuelles setzen der Cachezeiten | 1800 | 1800 | 1800 |
|
||||
| hide_langs_below | number | verberge Sprachen unter einem bestimmten Schwellwert | N/A | N/A | undefined |
|
||||
|
||||
> Hinweis bzgl. des Caches: Wenn die Anzahl der Forks und Stars geringer als 1Tsd ist, haben die Repo-Cards eine default Cachezeit von 30 Minuten (1800 Sekunden), ansonsten beträgt diese 2 Stunden (7200 Sekunden). Außerdem ist der Cache auf eine Minimum von 30 Minuten und ein Maximum von 24 Stunden begrenzt.
|
||||
|
||||
# GitHub Extra Pins
|
||||
|
||||
GitHub extra pins ermöglicht es, mit Hilfe eines GitHub-Readme-Profiles, mehr als 6 Repositories in deinen Profil anzuzeigen.
|
||||
|
||||
Und Bääm! Du bist nicht mehr auf 6 pinned Repositories limitiert.
|
||||
|
||||
### Benutzung
|
||||
|
||||
Füge diesen Code in deine Readme-Datei ein und passe die Links an.
|
||||
|
||||
Endpunkt: `api/pin?username=anuraghazra&repo=github-readme-stats`
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Beispiele
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
Benutze die [show_owner](#anpassungenpersonalisierung) Variable, um den Usernamen des Repo Eigentümers anzuzeigen.
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
# Top Programmiersprachen Card
|
||||
|
||||
Die Top Programmiersprachen Card visualisiert die am meisten benutzten Programmiersprachen eines GitHub-Nutzers.
|
||||
|
||||
_HINWEIS: Die Top Programmiersprachen treffen keine Aussage über persönliche Fähigkeiten oder der gleichen, es ist lediglich eine auf den GitHub-Statistiken des Nutzers basierende Kennzahl welche Programmiersprache wie häufig verwendet wurde._
|
||||
|
||||
### Benutzung
|
||||
|
||||
Füge diesen Code in deine Readme-Datei ein und passe die Links an.
|
||||
|
||||
Endpunkt: `api/top-langs?username=anuraghazra`
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Verberge Programmiersprachen unter einem bestimmten Schwellwert
|
||||
|
||||
Benutze den `?hide_langs_below=NUMBER` URL-Parameter um Programmiersprachen unter einem bestimmten prozentualen Schwellwert auszublenden.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Beispiel
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
---
|
||||
|
||||
### Alle Beispiele
|
||||
|
||||
- Default
|
||||
|
||||

|
||||
|
||||
- Ausblenden bestimmter Statistiken
|
||||
|
||||

|
||||
|
||||
- Icons anzeigen
|
||||
|
||||

|
||||
|
||||
- Erscheinungsbild/Themes
|
||||
|
||||
Choose from any of the [default themes](#themes)
|
||||
|
||||

|
||||
|
||||
- Stats Card anpassen
|
||||
|
||||

|
||||
|
||||
- Repo Card anpassen
|
||||
|
||||

|
||||
|
||||
- Top Programmiersprachen
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
---
|
||||
|
||||
### Kleiner Tip (Ausrichten der Repo Cards)
|
||||
|
||||
Üblicherweise ist es in `.md`-Dateien nicht möglich Bilder nebeneinander anzuzeigen. Um dies zu erreichen kann folgender Ansatz gewählt werden:
|
||||
|
||||
```md
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats">
|
||||
<img align="left" src="https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/convoychat">
|
||||
<img align="left" src="https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=convoychat" />
|
||||
</a>
|
||||
```
|
||||
|
||||
## Betreibe es auf deiner eigenen Vercel-Instanz
|
||||
|
||||
Da die GitHub API nur 5tsd Aufrufe pro Stunde zulässt, kann es passieren, dass meine `https://github-readme-stats.vercel.app/api` dieses Limit erreicht.
|
||||
Wenn du es auf deinem eigenen Vercel-Server hostest, brauchst du dich darum nicht zu kümmern. Klicke auf den Deploy-Button um loszulegen!
|
||||
|
||||
Hinweis: Seit [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) sollte es möglich sein mehr als 5tsd Aufrufe pro Stunde ohne Downtimes zu verkraften :D
|
||||
|
||||
[](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
<details>
|
||||
<summary>Anleitung zum Einrichten von Vercel</summary>
|
||||
|
||||
1. Gehe zu [vercel.com](https://vercel.com/)
|
||||
1. Klicke auf `Log in`
|
||||

|
||||
1. Melde dich mit deinem GitHub-account an, indem du `Continue with GitHub` klickst
|
||||

|
||||
1. Verbinde dich mit GitHub und erlaube den Zugriff auf alle Repositories, wenn gefordert
|
||||
1. Forke dieses Repository
|
||||
1. Gehe zurück zu deinem [Vercel dashboard](https://vercel.com/dashboard)
|
||||
1. Klick `Import Project`
|
||||

|
||||
1. Klick `Import Git Repository`
|
||||

|
||||
1. Wähle root und füge eine Umgebungsvariable namens PAT_1 (siehe Abbildung) die als Wert deinen persönlichen Access Token (PAT) hat hinzu, den du einfach [hier](https://github.com/settings/tokens/new) erzeugen kannst (lasse alles wie es ist, vergebe einen beliebigen Namen)
|
||||

|
||||
1. Klicke deploy, und das wars. Besuche deine domains um die API zu benutzen!
|
||||
</details>
|
||||
|
||||
## :sparkling_heart: Unterstütze das Projekt
|
||||
|
||||
Ich versuche alles was ich kann als Open-Source zur Verfügung zu stellen, als auch jedem der Hilfe bei der Benutzung dieses Projektes braucht zu antworten. Natürlich beansprucht sowas Zeit und Du kannst diesen Dienst kostenlos benutzen.
|
||||
|
||||
Dennoch, wenn Du dieses Projekt benutzt und damit zufrieden bist oder mich einfach nur motivieren möchtest weiterhin daran zu arbeiten, gibt es verschiedene Sachen die Du machen kannst:-
|
||||
|
||||
- Erwähne und verlinke das Projekt in deiner Readme wenn du es benutzt :D
|
||||
- Geb dem Projekt einen Stern hier auf GitHub und teile es :rocket:
|
||||
- [](https://www.paypal.me/anuraghazra) - Du kannst einmalige Spenden via PayPal tätigen. Ich kaufe mir wahrscheinlich einen ~~Kaffee~~ Tee davon. :tea:
|
||||
|
||||
Vielen Dank! :heart:
|
||||
|
||||
---
|
||||
|
||||
Mitarbeit an dem Projekt is immer Willkommen! <3
|
||||
|
||||
Gebaut mit :heart: und JavaScript.
|
||||
+317
@@ -0,0 +1,317 @@
|
||||
<p align="center">
|
||||
<img width="100px" src="https://res.cloudinary.com/anuraghazra/image/upload/v1594908242/logo_ccswme.svg" align="center" alt="GitHub Readme Stats" />
|
||||
<h2 align="center">GitHub Readme Stats</h2>
|
||||
<p align="center">¡Obtén tus estadísticas de GitHub generadas dinámicamente en tu README!</p>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/actions">
|
||||
<img alt="Tests Passing" src="https://github.com/anuraghazra/github-readme-stats/workflows/Test/badge.svg" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues">
|
||||
<img alt="Issues" src="https://img.shields.io/github/issues/anuraghazra/github-readme-stats?color=0088ff" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/pulls">
|
||||
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/anuraghazra/github-readme-stats?color=0088ff" />
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://a.paddle.com/v2/click/16413/119403?link=1227">
|
||||
<img src="https://img.shields.io/badge/Supported%20by-VSCode%20Power%20User%20%E2%86%92-gray.svg?colorA=655BE1&colorB=4F44D6&style=for-the-badge"/>
|
||||
</a>
|
||||
<a href="https://a.paddle.com/v2/click/16413/119403?link=2345">
|
||||
<img src="https://img.shields.io/badge/Supported%20by-Node%20Cli.com%20%E2%86%92-gray.svg?colorA=61c265&colorB=4CAF50&style=for-the-badge"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#ejemplo">Ve un ejemplo</a>
|
||||
·
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues/new/choose">Reporta un bug</a>
|
||||
·
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues/new/choose">Solicita una mejora</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="readme.md">English</a>
|
||||
·
|
||||
<a href="readme_cn.md">简体中文</a>
|
||||
·
|
||||
<a href="readme_ja.md">日本語</a>
|
||||
</p>
|
||||
</p>
|
||||
<p align="center">¿Te gusta este proyecto? ¡Por favor considera <a href="https://www.paypal.me/anuraghazra">donar</a> para ayudar a mejorarlo!
|
||||
|
||||
# Características
|
||||
|
||||
- [Tarjeta de estadísticas de GitHub](#tarjeta-de-estadísticas-de-github)
|
||||
- [Pins adicionales de GitHub](#pines-adicionales-de-github)
|
||||
- [Temas](#temas)
|
||||
- [Personalización](#personalización)
|
||||
- [Despliega por tu cuenta](#despliega-tu-propia-instancia-de-vercel)
|
||||
|
||||
# Tarjeta de estadísticas de GitHub
|
||||
|
||||
Copia y pega esto en el contenido de tu README.md y listo. ¡Simple!
|
||||
|
||||
Cambia el valor `?username=` al nombre de tu usuario de GitHub.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
_Nota: las clasificaciones se calculan basándose en las estadísticas del usuario. Ve [src/calculateRank.js](./src/calculateRank.js)._
|
||||
|
||||
### Ocultar estadísticas individualmente
|
||||
|
||||
Para ocultar alguna estadística específica, puedes utilizar el parámetro `?hide=` con un arreglo de items que quieras ocultar.
|
||||
|
||||
> Opciones: `&hide=["stars","commits","prs","issues","contribs"]`
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### Agregar contribuciones privadas al total de commits contados
|
||||
|
||||
Puede agregar el recuento de todas sus contribuciones privadas al recuento total de confirmaciones utilizando el parámetro de consulta `?count_private=true`.
|
||||
|
||||
_Nota: Si está desplegando este proyecto usted mismo, las contribuciones privadas se contarán de manera predeterminada; de lo contrario, deberá elegir compartir sus recuentos de contribuciones privadas._
|
||||
|
||||
> Opciones: `&count_private=true`
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### Mostrar íconos
|
||||
|
||||
Para habilitar los íconos, puedes utilizar `show_icons=true` como parámetro, de esta manera:
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### Temas
|
||||
|
||||
Puedes personalizar el aspecto de la tarjeta sin realizar ninguna [personalización manual](#personalización) con los temas incorporados.
|
||||
|
||||
Utiliza el parámetro `?theme=THEME_NAME`, de esta manera:
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
#### Todos los temas incorporados
|
||||
|
||||
dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula
|
||||
|
||||
<img src="https://res.cloudinary.com/anuraghazra/image/upload/v1595174536/grs-themes_l4ynja.png" alt="GitHub Readme Stat Themes" width="600px"/>
|
||||
|
||||
Puedes ver una vista previa de [todos los temas disponibles](./themes/README.md) o ver el [archivo de configuración](./themes/index.js) del tema y también **puedes contribuir con nuevos temas** si lo deseas :D
|
||||
|
||||
### Personalización
|
||||
|
||||
Puedes personalizar el aspecto de tu `Stats Card` o `Repo Card` de la manera que desees con los parámetros URL.
|
||||
|
||||
#### Opciones Comunes:
|
||||
|
||||
- `title_color` - Color del título _(hex color)_
|
||||
- `text_color` - Color del contenido _(hex color)_
|
||||
- `icon_color` - Color de icono si esta disponible _(hex color)_
|
||||
- `bg_color` - Color de fondo _(hex color)_
|
||||
- `theme` - Nombre del tema, elige uno de [todos los temas disponible ](./themes/README.md)
|
||||
- `cache_seconds` - Cache _(min: 1800, max: 86400)_
|
||||
|
||||
> Nota sobre la caché: las tarjetas de Repo tienen un caché predeterminado de 30 minutos (1800 segundos) si el recuento forks y el recuento de estrellas es inferior a 1k; de lo contrario, son 2 horas (7200). También tenga en cuenta que el caché está sujeto a un mínimo de 30 minutos y un máximo de 24 horas
|
||||
|
||||
#### Opciones exclusivas de la tarjeta de estadísticas:
|
||||
|
||||
- `hide` - Ocultar los elementos especificados de las estadísticas _ (valores separados por comas) _
|
||||
- `hide_title` - _(boolean)_
|
||||
- `hide_rank` - _(boolean)_
|
||||
- `show_icons` - _(boolean)_
|
||||
- `include_all_commits` - Cuente los commits totales en lugar de solo los commits del año actual _(boolean)_
|
||||
- `count_private` - Cuenta los commits privadas _(boolean)_
|
||||
- `line_height` - Establece el alto de línea entre texto _(number)_
|
||||
|
||||
#### Opciones exclusivas de la tarjeta Repo:
|
||||
|
||||
- `show_owner` - Mostrar el nombre del propietario del repositorio _(boolean)_
|
||||
|
||||
#### Opciones exclusivas de la tarjeta de lenguajes:
|
||||
|
||||
- `hide` - Ocultar los lenguajes especificados de la tarjeta _(valores separados por comas)_
|
||||
- `hide_title` - _(boolean)_
|
||||
- `layout` - Cambiar entre dos diseños disponibles `default` & `compact`
|
||||
- `card_width` - Establecer el ancho de la tarjeta manualmente _(number)_
|
||||
|
||||
> :warning: **Importante:**
|
||||
> Los nombres de los idiomas deben tener escape de uri, como se especifica en [Código porciento](https://es.wikipedia.org/wiki/C%C3%B3digo_porciento)
|
||||
> (es decir: `c++` debería convertirse en `c%2B%2B`,`jupyter notebook` debería convertirse en `jupyter%20notebook`, etc.)
|
||||
|
||||
---
|
||||
|
||||
# Pines adicionales de GitHub
|
||||
|
||||
Los pines adicionales de GitHub le permiten fijar más de 6 repositorios en su perfil utilizando un readme de perfil de GitHub.
|
||||
|
||||
Yey! Ya no está limitado a 6 repositorios anclados.
|
||||
|
||||
### Utilización
|
||||
|
||||
Copie y pegue este código en su archivo Léame y cambie los enlaces.
|
||||
|
||||
Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Ejemplo
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
Utiliza la variable [show_owner](#customización) para incluir el nombre de usuario del propietario del repositorio.
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
# Tarjeta de lenguajes principales
|
||||
|
||||
La tarjeta de lenguajes principales muestra los lenguajes principales del usuario de github que se han utilizado principalmente.
|
||||
|
||||
_NOTA: los lenguajes principales no indican mi nivel de habilidad o algo así, es una métrica de github de los lenguajes que tengo más código en github, es una nueva característica de github-readme-stats_
|
||||
|
||||
### Utilización
|
||||
|
||||
Copie y pegue este código en su readme y cambie los enlaces.
|
||||
|
||||
Endpoint: `api/top-langs?username=anuraghazra`
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Ocultar lenguajes individuales
|
||||
|
||||
Puede usar el parámetro `? Hide = language1, language2` para ocultar lenguajes individuales.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Diseño de tarjeta de lenguaje compacta
|
||||
|
||||
Puede usar la opción `& layout = compact` para cambiar el diseño de la tarjeta.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Ejemplo
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
- Diseño compacto
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
---
|
||||
|
||||
### Todos los ejemplos
|
||||
|
||||
- Por Defecto
|
||||
|
||||

|
||||
|
||||
- Ocultar estadísticas específicas
|
||||
|
||||

|
||||
|
||||
- Mostrando iconos
|
||||
|
||||

|
||||
|
||||
- Incluir todos los Commits
|
||||
|
||||

|
||||
|
||||
- Temas
|
||||
|
||||
Elija entre cualquiera de los [temas predeterminados](#themes)
|
||||
|
||||

|
||||
|
||||
- Personalizando tarjeta de estadísticas
|
||||
|
||||

|
||||
|
||||
- Personalizar la tarjeta de repositorio
|
||||
|
||||

|
||||
|
||||
- Lenguajes Principales
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
---
|
||||
|
||||
### Consejo rápido (alinee las tarjetas de repositorio)
|
||||
|
||||
Por lo general, no podrá diseñar las imágenes una al lado de la otra. Para hacerlo, puede usar este enfoque:
|
||||
|
||||
```md
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats">
|
||||
<img align="left" src="https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/convoychat">
|
||||
<img align="left" src="https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=convoychat" />
|
||||
</a>
|
||||
```
|
||||
|
||||
## Despliega tu propia instancia de vercel
|
||||
|
||||
Desde que la API de GitHub permite solo 5 mil peticiones por hora, es posible que mi `https://github-readme-stats.vercel.app/api` pueda llegar al límite. Si lo alojas en tu propio servidor de Vercel, no tendrás que preocuparte de nada. ¡Clickea en el botón "Deploy" para comenzar!
|
||||
|
||||
Nota: debido a esto [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) podríamos manejar más de 5 mil peticiones sin tener ningún problema con el downtime :D
|
||||
|
||||
[](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
<details>
|
||||
<summary>Guía para comenzar en Vercel</summary>
|
||||
|
||||
1. Ve a [vercel.com](https://vercel.com/)
|
||||
1. Clickea en `Log in`
|
||||

|
||||
1. Inicia sesión con GitHub presionando `Continue with GitHub`
|
||||

|
||||
1. Permite el acceso a todos los repositorios (si se te pregunta)
|
||||
1. Haz un Fork de este repositorio
|
||||
1. Dirígete de nuevo a tu [Vercel dashboard](https://vercel.com/dashboard)
|
||||
1. Selecciona `Import Project`
|
||||

|
||||
1. Selecciona `Import Git Repository`
|
||||

|
||||
1. Selecciona "root" y matén todo como está, simplemente añade tu variable de entorno llamada PAT_1 (como se muestra), la cual contendrá un token de acceso personal (PAT), el cual puedes crear fácilmente [aquí](https://github.com/settings/tokens/new) (mantén todo como está, simplemente asígnale un nombre, puede ser cualquiera que desees)
|
||||

|
||||
1. Clickea "Deploy" y ya está listo. ¡Ve tus dominios para usar la API!
|
||||
|
||||
</details>
|
||||
|
||||
## :sparkling_heart: Apoya al proyecto
|
||||
|
||||
Casi todos mis proyectos son código-abierto e intento responder a todos los usuarios que necesiten ayuda con alguno de estos proyectos, Obviamente,
|
||||
esto toma tiempo. Puedes usar este servicio gratis.
|
||||
|
||||
No obstante, si estás utilizando este proyecto y estás feliz con él o simplemente quieres animarme a que siga creando cosas, aquí tienes algunas maneras de hacerlo:
|
||||
|
||||
- Darme créditos cuando estés utilizando github-readme-stats en tu README, añadiendo un link a este repositorio :D
|
||||
- Dándole una estrella (starring) y compartiendo el proyecto :rocket:
|
||||
- [](https://www.paypal.me/anuraghazra) - Puedes hacerme una única donación a través de PayPal. Probablemente me compraré un ~~café~~ té. :tea:
|
||||
|
||||
¡Gracias! :heart:
|
||||
|
||||
---
|
||||
|
||||
¡Las contribuciones son bienvenidas! <3
|
||||
|
||||
Hecho con :heart: y JavaScript.
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
<p align="center">
|
||||
<img width="100px" src="https://res.cloudinary.com/anuraghazra/image/upload/v1594908242/logo_ccswme.svg" align="center" alt="GitHub Readme Stats" />
|
||||
<h2 align="center">GitHub Readme Stats</h2>
|
||||
<p align="center">あなたのREADMEに動的に生成されたGitHubの統計情報を載せましょう!</p>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/actions">
|
||||
<img alt="Tests Passing" src="https://github.com/anuraghazra/github-readme-stats/workflows/Test/badge.svg" />
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/anuraghazra/github-readme-stats">
|
||||
<img src="https://codecov.io/gh/anuraghazra/github-readme-stats/branch/master/graph/badge.svg" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues">
|
||||
<img alt="Issues" src="https://img.shields.io/github/issues/anuraghazra/github-readme-stats?color=0088ff" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/pulls">
|
||||
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/anuraghazra/github-readme-stats?color=0088ff" />
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://a.paddle.com/v2/click/16413/119403?link=1227">
|
||||
<img src="https://img.shields.io/badge/Supported%20by-VSCode%20Power%20User%20%E2%86%92-gray.svg?colorA=655BE1&colorB=4F44D6&style=for-the-badge"/>
|
||||
</a>
|
||||
<a href="https://a.paddle.com/v2/click/16413/119403?link=2345">
|
||||
<img src="https://img.shields.io/badge/Supported%20by-Node%20Cli.com%20%E2%86%92-gray.svg?colorA=61c265&colorB=4CAF50&style=for-the-badge"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#demo">View Demo</a>
|
||||
·
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues/new/choose">Report Bug</a>
|
||||
·
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues/new/choose">Request Feature</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="readme.md">English</a>
|
||||
·
|
||||
<a href="readme_cn.md">简体中文</a>
|
||||
·
|
||||
<a href="readme_es.md">Español</a>
|
||||
</p>
|
||||
</p>
|
||||
<p align="center">このプロジェクトを気に入っていただけましたか?<br>もしよろしければ、プロジェクトのさらなる改善のために<a href="https://www.paypal.me/anuraghazra">寄付</a>を検討して頂けると嬉しいです!</p>
|
||||
|
||||
# Features
|
||||
|
||||
- [GitHub Stats Card](#github-stats-card)
|
||||
- [GitHub Extra Pins](#github-extra-pins)
|
||||
- [Top Languages Card](#top-languages-card)
|
||||
- [Themes](#themes)
|
||||
- [Customization](#customization)
|
||||
- [Deploy Yourself](#deploy-on-your-own-vercel-instance)
|
||||
|
||||
# GitHub Stats Card
|
||||
|
||||
以下の構文をコピーして、あなたの Markdown ファイルに貼り付けるだけです。
|
||||
簡単ですね!
|
||||
|
||||
`?username=` の値は、あなたの GitHub アカウントのユーザー名に変更してください。
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
_Note: カードに表示されるランクはユーザの統計情報に基づいて計算されています。詳しくは、[src/calculateRank.js](./src/calculateRank.js)を見てください。_
|
||||
|
||||
### Hiding individual stats
|
||||
|
||||
クエリパラメータ `?hide=` にカンマ区切りの値を渡すことで、特定の統計情報を隠すことができます。
|
||||
|
||||
> Options: `&hide=stars,commits,prs,issues,contribs`
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### Adding private contributions count to total commits count
|
||||
|
||||
クエリパラメータ `?count_private=true` を使用することで、private contributions の数をコミット総数に追加することができます。
|
||||
|
||||
_Note: このプロジェクトを自分でデプロイしている場合、デフォルトでは非公開の貢献がカウントされます。_
|
||||
|
||||
> Options: `&count_private=true`
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### Showing icons
|
||||
|
||||
クエリパラメータ `?show_icons=true` を使用することで、アイコンが表示が有効になります。
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### Themes
|
||||
|
||||
内蔵されているテーマを使用することで、任意の[手動のカスタマイズ](#customization)を行うことなく、カードの外観をカスタマイズすることができます。
|
||||
|
||||
`?theme=THEME_NAME` は以下のように使います。
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
#### All inbuilt themes :-
|
||||
|
||||
dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula
|
||||
|
||||
<img src="https://res.cloudinary.com/anuraghazra/image/upload/v1595174536/grs-themes_l4ynja.png" alt="GitHub Readme Stat Themes" width="600px"/>
|
||||
|
||||
用意されている全てのテーマの[プレビュー](./themes/README.md)や[設定ファイル](./themes/index.js)を見ることができます。もしよろしければ、**新しいテーマを投稿してみてください** (´∀` )
|
||||
|
||||
### Customization
|
||||
|
||||
`Stats Card` や `Repo Card` の外観を URL パラメーターを使って好きなようにカスタマイズすることができます。
|
||||
|
||||
Customization Options:
|
||||
|
||||
| Option | type | description | Stats Card (default) | Repo Card (default) | Top Lang Card (default) |
|
||||
| ------------- | --------- | -------------------------------------------- | -------------------- | ------------------- | ----------------------- |
|
||||
| title_color | hex color | タイトルの色 | 2f80ed | 2f80ed | 2f80ed |
|
||||
| text_color | hex color | 文字の色 | 333 | 333 | 333 |
|
||||
| icon_color | hex color | アイコンの色 | 4c71f2 | 586069 | 586069 |
|
||||
| bg_color | hex color | カードの背景色 | FFFEFE | FFFEFE | FFFEFE |
|
||||
| line_height | number | 字間距離 | 30 | N/A | N/A |
|
||||
| hide | CSV | 項目の非表示 | undefined | N/A | undefined |
|
||||
| hide_rank | boolean | ranking の非表示 | false | N/A | N/A |
|
||||
| hide_title | boolean | タイトルの非表示 | false | N/A | false |
|
||||
| hide_border | boolean | 枠線の非表示 | false | N/A | N/A |
|
||||
| show_owner | boolean | オーナー名の表示 | N/A | false | N/A |
|
||||
| show_icons | boolean | アイコンの表示 | false | N/A | N/A |
|
||||
| theme | string | 用意されているテーマ | 'default' | 'default_repocard' | 'default' |
|
||||
| cache_seconds | number | キャッシュコントロール | 1800 | 1800 | 1800 |
|
||||
| count_private | boolean | private contributions 数をコミット総数に追加 | false | N/A | N/A |
|
||||
| layout | string | レイアウトのオプション選択 | N/A | N/A | 'default' |
|
||||
|
||||
> キャッシュに関する注意点: Repo cards のデフォルトのキャッシュは、フォーク数とスター数が 1k 未満の場合は 30 分(1800 秒) で、それ以外の場合は 2 時間(7200) です。また、キャッシュは最低でも 30 分、最大でも 24 時間に制限されていることに注意してください。
|
||||
|
||||
# GitHub Extra Pins
|
||||
|
||||
GitHub extra pins を使うと、GitHub の readme プロフィールを使って、自分のプロフィールに 6 つ以上のリポジトリをピン留めすることができます。
|
||||
|
||||
イェーイ! もはや 6 つのピン留めされたリポジトリに制限されることはありません。
|
||||
|
||||
### Usage
|
||||
|
||||
以下のコードをあなたの readme にコピー & ペーストし、リンクを変更してください。
|
||||
|
||||
Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Demo
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
リポジトリのオーナーのユーザー名を含める場合は、show_owner 変数を使用します。
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
# Top Languages Card
|
||||
|
||||
Top languages card には、その GitHub ユーザーが最も利用している Top languages が表示されます。
|
||||
|
||||
_NOTE: Top languages は、ユーザのスキルレベルを示すものではなく、GitHub 上でどの言語で最も多くのコードを書いているかを示す GitHub の指標です。_
|
||||
|
||||
### Usage
|
||||
|
||||
以下のコードをあなたの readme にコピー & ペーストし、リンクを変更してください。
|
||||
|
||||
Endpoint: `api/top-langs?username=anuraghazra`
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Hide individual languages
|
||||
|
||||
クエリパラメータ `?hide=language1,language2` 使用することで、個々の言語を非表示にすることができます。
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Compact Language Card Layout
|
||||
|
||||
クエリパラメータ `&layout=compact` を使用することで、カードのデザインを変更することができます。
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Demo
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
- Compact layout
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
---
|
||||
|
||||
### All Demos
|
||||
|
||||
- Default
|
||||
|
||||

|
||||
|
||||
- Hiding specific stats
|
||||
|
||||

|
||||
|
||||
- Showing icons
|
||||
|
||||

|
||||
|
||||
- Themes
|
||||
|
||||
任意の[テーマ](#themes)を選択できます。
|
||||
|
||||

|
||||
|
||||
- Customizing stats card
|
||||
|
||||

|
||||
|
||||
- Customizing repo card
|
||||
|
||||

|
||||
|
||||
- Top languages
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
---
|
||||
|
||||
### Quick Tip (Align The Repo Cards)
|
||||
|
||||
通常、画像を並べてレイアウトすることはできません。そのためには、次のような方法があります。
|
||||
|
||||
```md
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats">
|
||||
<img align="left" src="https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/convoychat">
|
||||
<img align="left" src="https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=convoychat" />
|
||||
</a>
|
||||
```
|
||||
|
||||
## Deploy on your own Vercel instance
|
||||
|
||||
GitHub API は 1 時間あたり 5k リクエストしか受け付けていないので、私の `https://github-readme-stats.vercel.app/api` がレートリミッターを超えてしまう可能性があります。自分の Vercel サーバーでホストしているのであれば、何も心配する必要はありません。デプロイボタンをクリックして始めましょう!
|
||||
|
||||
NOTE: [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) 以降は 5k 以上のリクエストに対応できるようになり、ダウンタイムの問題もなくなりました (´∀` )
|
||||
|
||||
[](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
<details>
|
||||
<summary>Vercelの設定ガイド</summary>
|
||||
|
||||
1. [vercel.com](https://vercel.com/)に行きます。
|
||||
1. `Log in`をクリックします。
|
||||

|
||||
1. `Continue with GitHub` を押して GitHub にサインインします。
|
||||

|
||||
1. GitHub にサインインし、すべてのリポジトリへのアクセスを許可します。
|
||||
1. このリポジトリをフォークします。
|
||||
1. [Vercel dashboard](https://vercel.com/dashboard)に戻ります。
|
||||
1. `Import Project` を選択します。
|
||||

|
||||
1. `Import Git Repository` を選択します。
|
||||

|
||||
1. root を選択して、すべてをそのままにしておき、PAT_1 という名前の環境変数を(下図のように)追加します。これには個人アクセストークン (PAT) が含まれており、[ここ](https://github.com/settings/tokens/new)で簡単に作成することができます (すべてをそのままにしておいて、何かに名前を付けてください。)
|
||||

|
||||
1. デプロイをクリックすれば完了です。API を使用するためにあなたのドメインを参照してください!
|
||||
|
||||
</details>
|
||||
|
||||
## :sparkling_heart: Support the project
|
||||
|
||||
私はできる限りのことをオープンソースで行い、これらのプロジェクトを利用して支援を必要としている皆さんに返信するようにしています。もちろんです。
|
||||
これは時間がかかります。無料でご利用いただけます。
|
||||
|
||||
しかし、もしあなたがこのプロジェクトを使っていて、それに満足しているのであれば、あるいは単に私にものを作り続けることを奨励したいのであれば、いくつかの方法があります。
|
||||
|
||||
- あなたの readme で github-readme-stats を使用して適切なクレジットを付与し、それにリンクします (´∀` )
|
||||
- 主演とプロジェクトの共有 :rocket:
|
||||
- [](https://www.paypal.me/anuraghazra) - PayPal を介して 1 回限りの寄付を行うことができます。私はおそらく ~~コーヒー~~ お茶買うでしょう。 :tea:
|
||||
|
||||
Thanks! :heart:
|
||||
|
||||
---
|
||||
|
||||
Contributions を歓迎します! <3
|
||||
|
||||
このプロジェクトは :heart: と JavaScript で作られています。
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
<p align="center">
|
||||
<img width="100px" src="https://res.cloudinary.com/anuraghazra/image/upload/v1594908242/logo_ccswme.svg" align="center" alt="GitHub Readme Stats" />
|
||||
<h2 align="center">GitHub Readme Stats</h2>
|
||||
<p align="center">Adicione suas estatísticas no GitHub geradas dinamicamente em seus readmes!</p>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/actions">
|
||||
<img alt="Testes aprovados" src="https://github.com/anuraghazra/github-readme-stats/workflows/Test/badge.svg" />
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/anuraghazra/github-readme-stats">
|
||||
<img src="https://codecov.io/gh/anuraghazra/github-readme-stats/branch/master/graph/badge.svg" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues">
|
||||
<img alt="Issues" src="https://img.shields.io/github/issues/anuraghazra/github-readme-stats?color=0088ff" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/pulls">
|
||||
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/anuraghazra/github-readme-stats?color=0088ff" />
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://a.paddle.com/v2/click/16413/119403?link=1227">
|
||||
<img src="https://img.shields.io/badge/Apoiado%20por-VSCode%20Power%20User%20%E2%86%92-gray.svg?colorA=655BE1&colorB=4F44D6&style=for-the-badge"/>
|
||||
</a>
|
||||
<a href="https://a.paddle.com/v2/click/16413/119403?link=2345">
|
||||
<img src="https://img.shields.io/badge/Apoiado%20por-Node%20Cli.com%20%E2%86%92-gray.svg?colorA=61c265&colorB=4CAF50&style=for-the-badge"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#demo">Ver demonstração</a>
|
||||
·
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues/new/choose">Reportar erros</a>
|
||||
·
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/issues/new/choose">Solicitar recursos</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="readme_cn.md">简体中文</a>
|
||||
·
|
||||
<a href="readme_es.md">Español</a>
|
||||
·
|
||||
<a href="readme_de.md">Deutsch</a>
|
||||
·
|
||||
<a href="readme_ja.md">日本語</a>
|
||||
·
|
||||
<a href="readme_pt-BR.md">Português Brasileiro</a>
|
||||
</p>
|
||||
</p>
|
||||
<p align="center">Gostou do projeto? Por favor considere <a href="https://www.paypal.me/anuraghazra">fazer uma doação</a> para ajudar a melhorá-lo!
|
||||
|
||||
# Características
|
||||
|
||||
- [Cartão de estatísticas do GitHub](#cartão-de-estatísticas-do-github)
|
||||
- [Pins extras do GitHub](#pins-extras-do-github)
|
||||
- [Cartão de principais linguagens de programação](#cartão-de-principais-linguagens-de-programação)
|
||||
- [Temas](#temas)
|
||||
- [Personalização](#personalização)
|
||||
- [Faça suas próprias implantações](#faça-suas-próprias-implantações)
|
||||
|
||||
# Cartão de estatísticas do GitHub
|
||||
|
||||
Copie e cole isso no seu conteúdo de remarcação e é isso. Simples!
|
||||
|
||||
Mude o valor de `?username=` para o seu nome de usuário no GitHub.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
_Nota: As classificações são baseadas nas estatísticas do usuário, veja [src/calculateRank.js](./src/calculateRank.js)_
|
||||
|
||||
### Ocultando estatísticas específicas
|
||||
|
||||
Para ocultar estatísticas individualmente, você pode passar um parâmetro de consulta `?hide=` com valores separados por vírgula.
|
||||
|
||||
> Opções: `&hide=stars,commits,prs,issues,contribs`
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### Adicionando contagem de contribuições privadas à contagem total de commits
|
||||
|
||||
Adicione a contagem de todas as suas contribuições privadas à contagem total de confirmações usando o parâmetro de consulta `?count_private=true`.
|
||||
|
||||
_Nota: Se você estiver implantando este projeto, as contribuições privadas serão contadas por padrão; caso contrário, você precisará compartilhar suas contagens de contribuições privadas._
|
||||
|
||||
> Opções: `&count_private=true`
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### Exibindo ícones
|
||||
|
||||
Para habilitar ícones, basta utilizar o parâmetro `show_icons=true` na sua requisição, da seguinte forma:
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### Temas
|
||||
|
||||
Com temas predefinidos, pode personalizar a aparência dos cartões sem precisar fazer nenhuma [configuração manual](#personalização).
|
||||
|
||||
Utilize o parâmetro `?theme=THEME_NAME`, da seguinte forma:
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
#### Todos os temas predefinidos :
|
||||
|
||||
dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula
|
||||
|
||||
<img src="https://res.cloudinary.com/anuraghazra/image/upload/v1595174536/grs-themes_l4ynja.png" alt="GitHub Readme Stat Themes" width="600px"/>
|
||||
|
||||
Visualize [todos o temas disponíveis](./themes/README.md) ou o [arquivo de configuração de tema](./themes/index.js), além de **também poder contribuir com novos temas**, se desejar :D
|
||||
|
||||
### Personalização
|
||||
|
||||
Personalize a aparência do seu `Stats Card` ou `Repo Card` da maneira que desejar com os parâmetros de URL.
|
||||
|
||||
#### Opções comuns
|
||||
|
||||
- `title_color` - Cor do título do cartão _(hex color)_
|
||||
- `text_color` - Cor de texto do conteúdo _(hex color)_
|
||||
- `icon_color` - Cor dos ícones (se disponível) _(hex color)_
|
||||
- `bg_color` - Cor de fundo do cartão _(hex color)_
|
||||
- `theme` - Nome do tema, escolha em [todos os temas disponíveis](./themes/README.md)
|
||||
- `cache_seconds` - Defina o cabeçalho do cache manualmente _(min: 1800, max: 86400)_
|
||||
|
||||
> Nota sobre o cache: Cartões de repositório tem um cache padrão de 30 minutos (1800 segundos), se o número a contagem de forks e contagem de estrelas é menor que 1 mil o padrão é 2 horas (7200). Note também que o cache é limitado a um mínimo de 30 minutos e um máximo de 24 horas.
|
||||
|
||||
#### Opções exclusivas do cartão de estatísticas:
|
||||
|
||||
- `hide` - Oculta itens específicos das estatísticas _(Valores separados por vírgulas)_
|
||||
- `hide_title` - Ocutar o título _(boolean)_
|
||||
- `hide_rank` - Ocultar a classificação _(boolean)_
|
||||
- `show_icons` - Mostrar ícones _(boolean)_
|
||||
- `include_all_commits` - Contabiliza todos os commits ao invés de apenas os atual ano _(boolean)_
|
||||
- `count_private` - Contabiliza commits privados _(boolean)_
|
||||
- `line_height` - Define a altura do espaçamento entre o texto _(number)_
|
||||
|
||||
#### Opções exclusivas do cartão de repositórios:
|
||||
|
||||
- `show_owner` - Exibir o nome da pessoa a quem o repositório pertence _(boolean)_
|
||||
|
||||
#### Opções exclusivas do cartão de linguagens:
|
||||
|
||||
- `hide` - Oculta linguagens específicas _(Valores separados por vírgulas)_
|
||||
- `hide_title` - Oculta o título _(boolean)_
|
||||
- `layout` - Alterna entre os dois layouts disponíveis `default` & `compact`
|
||||
- `card_width` - Define a largura do cartão manualmente _(number)_
|
||||
|
||||
> :warning: **Importante:**
|
||||
> Nomes de linguagens devem ser uma sequência escapada de URI, como específicado em [Codificação por cento](https://pt.wikipedia.org/wiki/Codificação_por_cento)
|
||||
> (Isso é: `c++` deve se tornar `c%2B%2B`, `jupyter notebook` deve se tornar `jupyter%20notebook`, etc.)
|
||||
|
||||
---
|
||||
|
||||
# Pins extras do GitHub
|
||||
|
||||
Os Pins extras do GitHub permitem fixar mais de 6 repositórios no seu perfil usando um perfil README.me do GitHub.
|
||||
|
||||
Uhu! Você não está mais limitado a 6 repositórios fixados.
|
||||
|
||||
### Utilização
|
||||
|
||||
Copie e cole esse código no seu README.md e altere os atributos.
|
||||
|
||||
Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Demonstração
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
Utilize a variável [show_owner](#personalização) para incluir o nome de usuário do proprietária do repositório
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
# Cartão de principais linguagens de programação
|
||||
|
||||
Exibe uma métrica de linguagens de programação mais usadas pelo usuário do GitHub.
|
||||
|
||||
_Nota: As principais linguagens de programação não fazem declarações sobre habilidades pessoais ou similares, é apenas uma figura-chave com base nas estatísticas do GitHub do usuário indicando a frequência com que cada uma foi utilizada._
|
||||
|
||||
### Utilização
|
||||
|
||||
Copie e cole esse código no seu README.md e altere os atributos.
|
||||
|
||||
Endpoint: `api/top-langs?username=anuraghazra`
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Ocultar linguagens individualmente
|
||||
|
||||
Utilize o parâmetro `?hide=language1,language2` para ocultar linguagens específicas.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Layout de cartão de linguagens compacto
|
||||
|
||||
Utilize a opção `&layout=compact` para mudar o layout do cartão.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Demonstração
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
- Layout compacto
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
---
|
||||
|
||||
### Todas as demonstrações
|
||||
|
||||
- Padronizado
|
||||
|
||||

|
||||
|
||||
- Ocultando estatísticas específicas
|
||||
|
||||

|
||||
|
||||
- Mostrando ícones
|
||||
|
||||

|
||||
|
||||
- Incluir todos os commits
|
||||
|
||||

|
||||
|
||||
- Temas
|
||||
|
||||
Escolha entre um dos [temas predefinidos](#temas)
|
||||
|
||||

|
||||
|
||||
- Personalizando o cartão de estatísticas
|
||||
|
||||

|
||||
|
||||
- Customizando o cartão de repositório
|
||||
|
||||

|
||||
|
||||
- Principais linguagens
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
---
|
||||
|
||||
### Dica (Alinhandos os cartões de repositório)
|
||||
|
||||
Por padrão, você não poderá organizar as imagens lado a lado. Para fazer isso, você pode usar a seguinte abordagem:
|
||||
|
||||
```md
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats">
|
||||
<img align="left" src="https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/convoychat">
|
||||
<img align="left" src="https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=convoychat" />
|
||||
</a>
|
||||
```
|
||||
|
||||
## Implante em sua própria instância do Vercel
|
||||
|
||||
Como a API do GitHub permite apenas 5 mil solicitações por hora, é possível que minha `https://github-readme-stats.vercel.app/api` atinja a cota limite. Se hospedar em seu próprio servidor Vercel, não precisará se preocupar com nada. Clique no botão de implantação para começar!
|
||||
|
||||
Nota: Desde [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) há possibilidade de lidar com mais de 5 mil chamadas por hora, sem interrupções :D
|
||||
|
||||
[](https://vercel.com/import/project?template=https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
<details>
|
||||
<summary>Guia de configuração do Vercel</summary>
|
||||
|
||||
1. Acesse [vercel.com](https://vercel.com/)
|
||||
1. Clique em `Login`
|
||||

|
||||
1. Acesse com o GitHub clicando em `Continue with GitHub`
|
||||

|
||||
1. Entre no GitHub e permita acesso a todos os repositórios, se solicitado
|
||||
1. Faça Fork neste repositório
|
||||
1. Volte ao seu [painel principal do Vercel](https://vercel.com/dashboard)
|
||||
1. Selecione `Import Project`
|
||||

|
||||
1. Selecione `Import Git Repository`
|
||||

|
||||
1. Selecione a raiz e mantenha tudo como está, basta adicionar sua variável de ambiente chamada PAT_1 (que será exibida), que conterá um token de acesso pessoal (PAT), que você pode criar facilmente [aqui](https://github.com/settings/tokens/new) (deixe tudo como está, apenas dê um nome, que pode ser o que você quiser)
|
||||

|
||||
1. Clique em `deploy` e já estará tudo pronto. Veja seus domínios para usar a API!
|
||||
|
||||
</details>
|
||||
|
||||
## :sparkling_heart: Apoie o projeto
|
||||
|
||||
|
||||
Disponibilizo como código aberto quase tudo o que posso e tento responder a todos que precisam de ajuda para utilizar esses projetos. Claro,
|
||||
isso demanda tempo. Utilize este serviço gratuitamente.
|
||||
|
||||
No entanto, se você utilizar este projeto e estiver satisfeito com ele, ou apenas quiser me encorajar a continuar criando coisas, existem algumas formas fazê-lo:
|
||||
|
||||
- Dando os devidos créditos ao usar github-readme-stats no seu README.me, adicionando uma referência ao projeto :D
|
||||
- Dando uma estrela (Starring) e compartilhando o projeto 🚀
|
||||
- [](https://www.paypal.me/anuraghazra) - Você pode fazer doações únicas via PayPal. Provavelmente vou comprar um ~~café~~ chá. :tea:
|
||||
|
||||
Obrigado! :heart:
|
||||
|
||||
---
|
||||
|
||||
Contribuições são bem-vindas! <3
|
||||
|
||||
Feito com :heart: e JavaScript.
|
||||
@@ -1,180 +0,0 @@
|
||||
/**
|
||||
* @file Script that can be used to close stale theme PRs that have a `invalid` label.
|
||||
*/
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
import { debug, setFailed } from "@actions/core";
|
||||
import github from "@actions/github";
|
||||
import { RequestError } from "@octokit/request-error";
|
||||
import { getGithubToken, getRepoInfo } from "./helpers.js";
|
||||
|
||||
const CLOSING_COMMENT = `
|
||||
\rThis theme PR has been automatically closed due to inactivity. Please reopen it if you want to continue working on it.\
|
||||
\rThank you for your contributions.
|
||||
`;
|
||||
const REVIEWER = "github-actions[bot]";
|
||||
|
||||
/**
|
||||
* Retrieve the review user.
|
||||
* @returns {string} review user.
|
||||
*/
|
||||
const getReviewer = () => {
|
||||
return process.env.REVIEWER ? process.env.REVIEWER : REVIEWER;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch open PRs from a given repository.
|
||||
*
|
||||
* @param {module:@actions/github.Octokit} octokit The octokit client.
|
||||
* @param {string} user The user name of the repository owner.
|
||||
* @param {string} repo The name of the repository.
|
||||
* @param {string} reviewer The reviewer to filter by.
|
||||
* @returns {Promise<Object[]>} The open PRs.
|
||||
*/
|
||||
export const fetchOpenPRs = async (octokit, user, repo, reviewer) => {
|
||||
const openPRs = [];
|
||||
let hasNextPage = true;
|
||||
let endCursor;
|
||||
while (hasNextPage) {
|
||||
try {
|
||||
const { repository } = await octokit.graphql(
|
||||
`
|
||||
{
|
||||
repository(owner: "${user}", name: "${repo}") {
|
||||
open_prs: pullRequests(${
|
||||
endCursor ? `after: "${endCursor}", ` : ""
|
||||
}
|
||||
first: 100, states: OPEN, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
nodes {
|
||||
number
|
||||
commits(last:1){
|
||||
nodes{
|
||||
commit{
|
||||
pushedDate
|
||||
}
|
||||
}
|
||||
}
|
||||
labels(first: 100, orderBy:{field: CREATED_AT, direction: DESC}) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
reviews(first: 100, states: CHANGES_REQUESTED, author: "${reviewer}") {
|
||||
nodes {
|
||||
submittedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
endCursor
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
);
|
||||
openPRs.push(...repository.open_prs.nodes);
|
||||
hasNextPage = repository.open_prs.pageInfo.hasNextPage;
|
||||
endCursor = repository.open_prs.pageInfo.endCursor;
|
||||
} catch (error) {
|
||||
if (error instanceof RequestError) {
|
||||
setFailed(`Could not retrieve top PRs using GraphQl: ${error.message}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return openPRs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve pull requests that have a given label.
|
||||
*
|
||||
* @param {Object[]} pulls The pull requests to check.
|
||||
* @param {string} label The label to check for.
|
||||
* @returns {Object[]} The pull requests that have the given label.
|
||||
*/
|
||||
export const pullsWithLabel = (pulls, label) => {
|
||||
return pulls.filter((pr) => {
|
||||
return pr.labels.nodes.some((lab) => lab.name === label);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if PR is stale. Meaning that it hasn't been updated in a given time.
|
||||
*
|
||||
* @param {Object} pullRequest request object.
|
||||
* @param {number} staleDays number of days.
|
||||
* @returns {boolean} indicating if PR is stale.
|
||||
*/
|
||||
const isStale = (pullRequest, staleDays) => {
|
||||
const lastCommitDate = new Date(
|
||||
pullRequest.commits.nodes[0].commit.pushedDate,
|
||||
);
|
||||
if (pullRequest.reviews.nodes[0]) {
|
||||
const lastReviewDate = new Date(
|
||||
pullRequest.reviews.nodes.sort((a, b) => (a < b ? 1 : -1))[0].submittedAt,
|
||||
);
|
||||
const lastUpdateDate =
|
||||
lastCommitDate >= lastReviewDate ? lastCommitDate : lastReviewDate;
|
||||
const now = new Date();
|
||||
return (now - lastUpdateDate) / (1000 * 60 * 60 * 24) >= staleDays;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Main function.
|
||||
*
|
||||
* @returns {Promise<void>} A promise.
|
||||
*/
|
||||
const run = async () => {
|
||||
try {
|
||||
// Create octokit client.
|
||||
const dryRun = process.env.DRY_RUN === "true" || false;
|
||||
const staleDays = process.env.STALE_DAYS || 20;
|
||||
debug("Creating octokit client...");
|
||||
const octokit = github.getOctokit(getGithubToken());
|
||||
const { owner, repo } = getRepoInfo(github.context);
|
||||
const reviewer = getReviewer();
|
||||
|
||||
// Retrieve all theme pull requests.
|
||||
debug("Retrieving all theme pull requests...");
|
||||
const prs = await fetchOpenPRs(octokit, owner, repo, reviewer);
|
||||
const themePRs = pullsWithLabel(prs, "themes");
|
||||
const invalidThemePRs = pullsWithLabel(themePRs, "invalid");
|
||||
debug("Retrieving stale theme PRs...");
|
||||
const staleThemePRs = invalidThemePRs.filter((pr) =>
|
||||
isStale(pr, staleDays),
|
||||
);
|
||||
const staleThemePRsNumbers = staleThemePRs.map((pr) => pr.number);
|
||||
debug(`Found ${staleThemePRs.length} stale theme PRs`);
|
||||
|
||||
// Loop through all stale invalid theme pull requests and close them.
|
||||
for (const prNumber of staleThemePRsNumbers) {
|
||||
debug(`Closing #${prNumber} because it is stale...`);
|
||||
if (dryRun) {
|
||||
debug("Dry run enabled, skipping...");
|
||||
} else {
|
||||
await octokit.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: CLOSING_COMMENT,
|
||||
});
|
||||
await octokit.rest.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber,
|
||||
state: "closed",
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setFailed(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
@@ -1,30 +0,0 @@
|
||||
import axios from "axios";
|
||||
import fs from "fs";
|
||||
import jsYaml from "js-yaml";
|
||||
|
||||
const LANGS_FILEPATH = "./src/common/languageColors.json";
|
||||
|
||||
//Retrieve languages from github linguist repository yaml file
|
||||
//@ts-ignore
|
||||
axios
|
||||
.get(
|
||||
"https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml",
|
||||
)
|
||||
.then((response) => {
|
||||
//and convert them to a JS Object
|
||||
const languages = jsYaml.load(response.data);
|
||||
|
||||
const languageColors = {};
|
||||
|
||||
//Filter only language colors from the whole file
|
||||
Object.keys(languages).forEach((lang) => {
|
||||
languageColors[lang] = languages[lang].color;
|
||||
});
|
||||
|
||||
//Debug Print
|
||||
//console.dir(languageColors);
|
||||
fs.writeFileSync(
|
||||
LANGS_FILEPATH,
|
||||
JSON.stringify(languageColors, null, " "),
|
||||
);
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import fs from "fs";
|
||||
import { themes } from "../themes/index.js";
|
||||
const theme = require("../themes/index");
|
||||
const fs = require("fs");
|
||||
|
||||
const TARGET_FILE = "./themes/README.md";
|
||||
const REPO_CARD_LINKS_FLAG = "<!-- REPO_CARD_LINKS -->";
|
||||
@@ -12,17 +12,17 @@ const THEME_TEMPLATE = `## Available Themes
|
||||
|
||||
<!-- DO NOT EDIT THIS FILE DIRECTLY -->
|
||||
|
||||
With inbuilt themes, you can customize the look of the card without doing any manual customization.
|
||||
With inbuilt themes you can customize the look of the card without doing any manual customization.
|
||||
|
||||
Use \`?theme=THEME_NAME\` parameter like so:
|
||||
Use \`?theme=THEME_NAME\` parameter like so :-
|
||||
|
||||
\`\`\`md
|
||||

|
||||

|
||||
\`\`\`
|
||||
|
||||
## Stats
|
||||
|
||||
> These themes work with all five of our cards: Stats Card, Repo Card, Gist Card, Top Languages Card, and WakaTime Card.
|
||||
> These themes work both for the Stats Card and Repo Card.
|
||||
|
||||
| | | |
|
||||
| :--: | :--: | :--: |
|
||||
@@ -30,7 +30,7 @@ ${STAT_CARD_TABLE_FLAG}
|
||||
|
||||
## Repo Card
|
||||
|
||||
> These themes work with all five of our cards: Stats Card, Repo Card, Gist Card, Top Languages Card, and WakaTime Card.
|
||||
> These themes work both for the Stats Card and Repo Card.
|
||||
|
||||
| | | |
|
||||
| :--: | :--: | :--: |
|
||||
@@ -39,6 +39,11 @@ ${REPO_CARD_TABLE_FLAG}
|
||||
${STAT_CARD_LINKS_FLAG}
|
||||
|
||||
${REPO_CARD_LINKS_FLAG}
|
||||
|
||||
|
||||
[add-theme]: https://github.com/anuraghazra/github-readme-stats/edit/master/themes/index.js
|
||||
|
||||
Wanted to add a new theme? Consider reading the [contribution guidelines](../CONTRIBUTING.md#themes-contribution) :D
|
||||
`;
|
||||
|
||||
const createRepoMdLink = (theme) => {
|
||||
@@ -49,34 +54,39 @@ const createStatMdLink = (theme) => {
|
||||
};
|
||||
|
||||
const generateLinks = (fn) => {
|
||||
return Object.keys(themes)
|
||||
return Object.keys(theme)
|
||||
.map((name) => fn(name))
|
||||
.join("");
|
||||
};
|
||||
|
||||
const createTableItem = ({ link, label, isRepoCard }) => {
|
||||
if (!link || !label) {
|
||||
return "";
|
||||
}
|
||||
if (!link || !label) return "";
|
||||
return `\`${label}\` ![${link}][${link}${isRepoCard ? "_repo" : ""}]`;
|
||||
};
|
||||
|
||||
const generateTable = ({ isRepoCard }) => {
|
||||
const rows = [];
|
||||
const themesFiltered = Object.keys(themes).filter(
|
||||
(name) => name !== (isRepoCard ? "default" : "default_repocard"),
|
||||
const themes = Object.keys(theme).filter(
|
||||
(name) => name !== (!isRepoCard ? "default_repocard" : "default")
|
||||
);
|
||||
|
||||
for (let i = 0; i < themesFiltered.length; i += 3) {
|
||||
const one = themesFiltered[i];
|
||||
const two = themesFiltered[i + 1];
|
||||
const three = themesFiltered[i + 2];
|
||||
for (let i = 0; i < themes.length; i += 3) {
|
||||
const one = themes[i];
|
||||
const two = themes[i + 1];
|
||||
const three = themes[i + 2];
|
||||
|
||||
let tableItem1 = createTableItem({ link: one, label: one, isRepoCard });
|
||||
let tableItem2 = createTableItem({ link: two, label: two, isRepoCard });
|
||||
let tableItem3 = createTableItem({ link: three, label: three, isRepoCard });
|
||||
|
||||
if (three === undefined) {
|
||||
tableItem3 = `[Add your theme][add-theme]`;
|
||||
}
|
||||
rows.push(`| ${tableItem1} | ${tableItem2} | ${tableItem3} |`);
|
||||
|
||||
// if it's the last row & the row has no empty space push a new row
|
||||
if (three && i + 3 === themes.length) {
|
||||
rows.push(`| [Add your theme][add-theme] | | |`);
|
||||
}
|
||||
}
|
||||
|
||||
return rows.join("\n");
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* @file Contains helper functions used in the scripts.
|
||||
*/
|
||||
|
||||
import { getInput } from "@actions/core";
|
||||
|
||||
const OWNER = "anuraghazra";
|
||||
const REPO = "github-readme-stats";
|
||||
|
||||
/**
|
||||
* Retrieve information about the repository that ran the action.
|
||||
*
|
||||
* @param {Object} ctx Action context.
|
||||
* @returns {Object} Repository information.
|
||||
*/
|
||||
export const getRepoInfo = (ctx) => {
|
||||
try {
|
||||
return {
|
||||
owner: ctx.repo.owner,
|
||||
repo: ctx.repo.repo,
|
||||
};
|
||||
} catch (error) {
|
||||
// Resolve eslint no-unused-vars
|
||||
error;
|
||||
|
||||
return {
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve github token and throw error if it is not found.
|
||||
*
|
||||
* @returns {string} GitHub token.
|
||||
*/
|
||||
export const getGithubToken = () => {
|
||||
const token = getInput("github_token") || process.env.GITHUB_TOKEN;
|
||||
if (!token) {
|
||||
throw Error("Could not find github token");
|
||||
}
|
||||
return token;
|
||||
};
|
||||
+55
-665
@@ -1,687 +1,77 @@
|
||||
/**
|
||||
* @file This script is used to preview the theme on theme PRs.
|
||||
*/
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
const core = require("@actions/core");
|
||||
const github = require("@actions/github");
|
||||
const parse = require("parse-diff");
|
||||
require("dotenv").config();
|
||||
|
||||
import { debug, setFailed } from "@actions/core";
|
||||
import github from "@actions/github";
|
||||
import ColorContrastChecker from "color-contrast-checker";
|
||||
import { info } from "console";
|
||||
import Hjson from "hjson";
|
||||
import snakeCase from "lodash.snakecase";
|
||||
import parse from "parse-diff";
|
||||
import { inspect } from "util";
|
||||
import { isValidHexColor, isValidGradient } from "../src/common/color.js";
|
||||
import { themes } from "../themes/index.js";
|
||||
import { getGithubToken, getRepoInfo } from "./helpers.js";
|
||||
|
||||
const COMMENTER = "github-actions[bot]";
|
||||
|
||||
const COMMENT_TITLE = "Automated Theme Preview";
|
||||
const THEME_PR_FAIL_TEXT = ":x: Theme PR does not adhere to our guidelines.";
|
||||
const THEME_PR_SUCCESS_TEXT =
|
||||
":heavy_check_mark: Theme PR does adhere to our guidelines.";
|
||||
const FAIL_TEXT = `
|
||||
\rUnfortunately, your theme PR contains an error or does not adhere to our [theme guidelines](https://github.com/anuraghazra/github-readme-stats/blob/master/CONTRIBUTING.md#themes-contribution). Please fix the issues below, and we will review your\
|
||||
\r PR again. This pull request will **automatically close in 20 days** if no changes are made. After this time, you must re-open the PR for it to be reviewed.
|
||||
`;
|
||||
const THEME_CONTRIB_GUIDELINES = `
|
||||
\rHi, thanks for the theme contribution. Please read our theme [contribution guidelines](https://github.com/anuraghazra/github-readme-stats/blob/master/CONTRIBUTING.md#themes-contribution).
|
||||
|
||||
\r> [!WARNING]\
|
||||
\r> Keep in mind that we already have a vast collection of different themes. To keep their number manageable, we began to add only themes supported by the community. Your pull request with theme addition will be merged once we get enough positive feedback from the community in the form of thumbs up :+1: emojis (see [#1935](https://github.com/anuraghazra/github-readme-stats/issues/1935#top-themes-prs)). We expect to see at least 10-15 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.
|
||||
|
||||
\r> [!WARNING]\
|
||||
\r> Please do not submit a pull request with a batch of themes, since it will be hard to judge how the community will react to each of them. We will only merge one theme per pull request. If you have several themes, please submit a separate pull request for each of them. Situations when you have several versions of the same theme (e.g. light and dark) are an exception to this rule.
|
||||
|
||||
\r> [!NOTE]\
|
||||
\r> Also, note that if this theme is exclusively for your personal use, then instead of adding it to our theme collection, you can use card [customization options](https://github.com/anuraghazra/github-readme-stats#customization).
|
||||
`;
|
||||
const COLOR_PROPS = {
|
||||
title_color: 6,
|
||||
icon_color: 6,
|
||||
text_color: 6,
|
||||
bg_color: 23,
|
||||
border_color: 6,
|
||||
};
|
||||
const ACCEPTED_COLOR_PROPS = Object.keys(COLOR_PROPS);
|
||||
const REQUIRED_COLOR_PROPS = ACCEPTED_COLOR_PROPS.slice(0, 4);
|
||||
const INVALID_REVIEW_COMMENT = (commentUrl) =>
|
||||
`Some themes are invalid. See the [Automated Theme Preview](${commentUrl}) comment above for more information.`;
|
||||
var OCTOKIT;
|
||||
var OWNER;
|
||||
var REPO;
|
||||
var PULL_REQUEST_ID;
|
||||
|
||||
/**
|
||||
* Incorrect JSON format error.
|
||||
* @extends Error
|
||||
* @param {string} message Error message.
|
||||
* @returns {Error} IncorrectJsonFormatError.
|
||||
*/
|
||||
class IncorrectJsonFormatError extends Error {
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param {string} message Error message.
|
||||
*/
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "IncorrectJsonFormatError";
|
||||
const parsePullRequestId = (githubRef) => {
|
||||
const result = /refs\/pull\/(\d+)\/merge/g.exec(githubRef);
|
||||
if (!result) {
|
||||
console.log("Reference not found.");
|
||||
return 297;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve PR number from the event payload.
|
||||
*
|
||||
* @returns {number} PR number.
|
||||
*/
|
||||
const getPrNumber = () => {
|
||||
if (process.env.MOCK_PR_NUMBER) {
|
||||
return process.env.MOCK_PR_NUMBER; // For testing purposes.
|
||||
}
|
||||
|
||||
const pullRequest = github.context.payload.pull_request;
|
||||
if (!pullRequest) {
|
||||
throw Error("Could not get pull request number from context");
|
||||
}
|
||||
return pullRequest.number;
|
||||
const [, pullRequestId] = result;
|
||||
return pullRequestId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the commenting user.
|
||||
* @returns {string} Commenting user.
|
||||
*/
|
||||
const getCommenter = () => {
|
||||
return process.env.COMMENTER ? process.env.COMMENTER : COMMENTER;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether the comment is a preview comment.
|
||||
*
|
||||
* @param {Object} inputs Action inputs.
|
||||
* @param {Object} comment Comment object.
|
||||
* @returns {boolean} Whether the comment is a preview comment.
|
||||
*/
|
||||
const isPreviewComment = (inputs, comment) => {
|
||||
return (
|
||||
(inputs.commentAuthor && comment.user
|
||||
? comment.user.login === inputs.commentAuthor
|
||||
: true) &&
|
||||
(inputs.bodyIncludes && comment.body
|
||||
? comment.body.includes(inputs.bodyIncludes)
|
||||
: true)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the preview theme comment.
|
||||
*
|
||||
* @param {Object} octokit Octokit instance.
|
||||
* @param {number} issueNumber Issue number.
|
||||
* @param {string} owner Owner of the repository.
|
||||
* @param {string} repo Repository name.
|
||||
* @param {string} commenter Comment author.
|
||||
* @returns {Object | undefined} The GitHub comment object.
|
||||
*/
|
||||
const findComment = async (octokit, issueNumber, owner, repo, commenter) => {
|
||||
const parameters = {
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
};
|
||||
const inputs = {
|
||||
commentAuthor: commenter,
|
||||
bodyIncludes: COMMENT_TITLE,
|
||||
};
|
||||
|
||||
// Search each page for the comment
|
||||
for await (const { data: comments } of octokit.paginate.iterator(
|
||||
octokit.rest.issues.listComments,
|
||||
parameters,
|
||||
)) {
|
||||
const comment = comments.find((comment) =>
|
||||
isPreviewComment(inputs, comment),
|
||||
);
|
||||
if (comment) {
|
||||
debug(`Found theme preview comment: ${inspect(comment)}`);
|
||||
return comment;
|
||||
} else {
|
||||
debug(`No theme preview comment found.`);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create or update the preview comment.
|
||||
*
|
||||
* @param {Object} octokit Octokit instance.
|
||||
* @param {number} issueNumber Issue number.
|
||||
* @param {Object} repo Repository name.
|
||||
* @param {Object} owner Owner of the repository.
|
||||
* @param {number} commentId Comment ID.
|
||||
* @param {string} body Comment body.
|
||||
* @returns {string} The comment URL.
|
||||
*/
|
||||
const upsertComment = async (
|
||||
octokit,
|
||||
issueNumber,
|
||||
repo,
|
||||
owner,
|
||||
commentId,
|
||||
body,
|
||||
) => {
|
||||
let resp;
|
||||
if (commentId === undefined) {
|
||||
resp = await octokit.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
resp = await octokit.rest.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: commentId,
|
||||
body,
|
||||
});
|
||||
}
|
||||
return resp.data.html_url;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a review to the pull request.
|
||||
*
|
||||
* @param {Object} octokit Octokit instance.
|
||||
* @param {number} prNumber Pull request number.
|
||||
* @param {string} owner Owner of the repository.
|
||||
* @param {string} repo Repository name.
|
||||
* @param {string} reviewState The review state. Options are (APPROVE, REQUEST_CHANGES, COMMENT, PENDING).
|
||||
* @param {string} reason The reason for the review.
|
||||
* @returns {Promise<void>} Promise.
|
||||
*/
|
||||
const addReview = async (
|
||||
octokit,
|
||||
prNumber,
|
||||
owner,
|
||||
repo,
|
||||
reviewState,
|
||||
reason,
|
||||
) => {
|
||||
await octokit.rest.pulls.createReview({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber,
|
||||
event: reviewState,
|
||||
body: reason,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add label to pull request.
|
||||
*
|
||||
* @param {Object} octokit Octokit instance.
|
||||
* @param {number} prNumber Pull request number.
|
||||
* @param {string} owner Repository owner.
|
||||
* @param {string} repo Repository name.
|
||||
* @param {string[]} labels Labels to add.
|
||||
* @returns {Promise<void>} Promise.
|
||||
*/
|
||||
const addLabel = async (octokit, prNumber, owner, repo, labels) => {
|
||||
await octokit.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
labels,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove label from the pull request.
|
||||
*
|
||||
* @param {Object} octokit Octokit instance.
|
||||
* @param {number} prNumber Pull request number.
|
||||
* @param {string} owner Repository owner.
|
||||
* @param {string} repo Repository name.
|
||||
* @param {string} label Label to add or remove.
|
||||
* @returns {Promise<void>} Promise.
|
||||
*/
|
||||
const removeLabel = async (octokit, prNumber, owner, repo, label) => {
|
||||
await octokit.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
name: label,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds or removes a label from the pull request.
|
||||
*
|
||||
* @param {Object} octokit Octokit instance.
|
||||
* @param {number} prNumber Pull request number.
|
||||
* @param {string} owner Repository owner.
|
||||
* @param {string} repo Repository name.
|
||||
* @param {string} label Label to add or remove.
|
||||
* @param {boolean} add Whether to add or remove the label.
|
||||
* @returns {Promise<void>} Promise.
|
||||
*/
|
||||
const addRemoveLabel = async (octokit, prNumber, owner, repo, label, add) => {
|
||||
const res = await octokit.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber,
|
||||
});
|
||||
if (add) {
|
||||
if (!res.data.labels.find((l) => l.name === label)) {
|
||||
await addLabel(octokit, prNumber, owner, repo, [label]);
|
||||
}
|
||||
} else {
|
||||
if (res.data.labels.find((l) => l.name === label)) {
|
||||
await removeLabel(octokit, prNumber, owner, repo, label);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve webAim contrast color check link.
|
||||
*
|
||||
* @param {string} color1 First color.
|
||||
* @param {string} color2 Second color.
|
||||
* @returns {string} WebAim contrast color check link.
|
||||
*/
|
||||
const getWebAimLink = (color1, color2) => {
|
||||
return `https://webaim.org/resources/contrastchecker/?fcolor=${color1}&bcolor=${color2}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the theme GRS url.
|
||||
*
|
||||
* @param {Object} colors The theme colors.
|
||||
* @returns {string} GRS theme url.
|
||||
*/
|
||||
const getGRSLink = (colors) => {
|
||||
const url = `https://github-readme-stats.vercel.app/api?username=anuraghazra`;
|
||||
const colorString = Object.keys(colors)
|
||||
.map((colorKey) => `${colorKey}=${colors[colorKey]}`)
|
||||
.join("&");
|
||||
|
||||
return `${url}&${colorString}&show_icons=true`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve javascript object from json string.
|
||||
*
|
||||
* @description Wraps the Hjson parse function to fix several known json syntax errors.
|
||||
*
|
||||
* @param {string} json The json to parse.
|
||||
* @returns {Object} Object parsed from the json.
|
||||
*/
|
||||
const parseJSON = (json) => {
|
||||
async function run() {
|
||||
try {
|
||||
const parsedJson = Hjson.parse(json);
|
||||
if (typeof parsedJson === "object") {
|
||||
return parsedJson;
|
||||
} else {
|
||||
throw new IncorrectJsonFormatError(
|
||||
"PR diff is not a valid theme JSON object.",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Resolve eslint no-unused-vars
|
||||
error;
|
||||
const token = core.getInput("token");
|
||||
const octokit = github.getOctokit(token || process.env.PERSONAL_TOKEN);
|
||||
const pullRequestId = parsePullRequestId(process.env.GITHUB_REF);
|
||||
|
||||
// Remove trailing commas (if any).
|
||||
let parsedJson = json.replace(/(,\s*})/g, "}");
|
||||
|
||||
// Remove JS comments (if any).
|
||||
parsedJson = parsedJson.replace(/\/\/[A-z\s]*\s/g, "");
|
||||
|
||||
// Fix incorrect open bracket (if any).
|
||||
const splitJson = parsedJson
|
||||
.split(/([\s\r\s]*}[\s\r\s]*,[\s\r\s]*)(?=[\w"-]+:)/)
|
||||
.filter((x) => typeof x !== "string" || !!x.trim()); // Split json into array of strings and objects.
|
||||
if (splitJson[0].replace(/\s+/g, "") === "},") {
|
||||
splitJson[0] = "},";
|
||||
if (/\s*}\s*,?\s*$/.test(splitJson[1])) {
|
||||
splitJson.shift();
|
||||
} else {
|
||||
splitJson.push(splitJson.shift());
|
||||
}
|
||||
parsedJson = splitJson.join("");
|
||||
}
|
||||
|
||||
// Try to parse the fixed json.
|
||||
try {
|
||||
return Hjson.parse(parsedJson);
|
||||
} catch (error) {
|
||||
throw new IncorrectJsonFormatError(
|
||||
`Theme JSON file could not be parsed: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check whether the theme name is still available.
|
||||
* @param {string} name Theme name.
|
||||
* @returns {boolean} Whether the theme name is available.
|
||||
*/
|
||||
const themeNameAlreadyExists = (name) => {
|
||||
return themes[name] !== undefined;
|
||||
};
|
||||
|
||||
const DRY_RUN = process.env.DRY_RUN === "true" || false;
|
||||
|
||||
/**
|
||||
* Main function.
|
||||
*
|
||||
* @returns {Promise<void>} Promise.
|
||||
*/
|
||||
export const run = async () => {
|
||||
try {
|
||||
debug("Retrieve action information from context...");
|
||||
debug(`Context: ${inspect(github.context)}`);
|
||||
let commentBody = `
|
||||
\r# ${COMMENT_TITLE}
|
||||
\r${THEME_CONTRIB_GUIDELINES}
|
||||
`;
|
||||
const ccc = new ColorContrastChecker();
|
||||
OCTOKIT = github.getOctokit(getGithubToken());
|
||||
PULL_REQUEST_ID = getPrNumber();
|
||||
const { owner, repo } = getRepoInfo(github.context);
|
||||
OWNER = owner;
|
||||
REPO = repo;
|
||||
const commenter = getCommenter();
|
||||
PULL_REQUEST_ID = getPrNumber();
|
||||
debug(`Owner: ${OWNER}`);
|
||||
debug(`Repo: ${REPO}`);
|
||||
debug(`Commenter: ${commenter}`);
|
||||
|
||||
// Retrieve the PR diff and preview-theme comment.
|
||||
debug("Retrieve PR diff...");
|
||||
const res = await OCTOKIT.rest.pulls.get({
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
pull_number: PULL_REQUEST_ID,
|
||||
let res = await octokit.pulls.get({
|
||||
owner: "anuraghazra",
|
||||
repo: "github-readme-stats",
|
||||
pull_number: pullRequestId,
|
||||
mediaType: {
|
||||
format: "diff",
|
||||
},
|
||||
});
|
||||
debug("Retrieve preview-theme comment...");
|
||||
const comment = await findComment(
|
||||
OCTOKIT,
|
||||
PULL_REQUEST_ID,
|
||||
OWNER,
|
||||
REPO,
|
||||
commenter,
|
||||
);
|
||||
|
||||
// Retrieve theme changes from the PR diff.
|
||||
debug("Retrieve themes...");
|
||||
const diff = parse(res.data);
|
||||
|
||||
// Retrieve all theme changes from the PR diff and convert to JSON.
|
||||
debug("Retrieve theme changes...");
|
||||
const content = diff
|
||||
let diff = parse(res.data);
|
||||
let colorStrings = diff
|
||||
.find((file) => file.to === "themes/index.js")
|
||||
.chunks.map((chunk) =>
|
||||
chunk.changes
|
||||
.filter((c) => c.type === "add")
|
||||
.map((c) => c.content.replace("+", ""))
|
||||
.join(""),
|
||||
)
|
||||
.chunks[0].changes.filter((c) => c.type === "add")
|
||||
.map((c) => c.content.replace("+", ""))
|
||||
.join("");
|
||||
const themeObject = parseJSON(content);
|
||||
if (
|
||||
Object.keys(themeObject).every(
|
||||
(key) => typeof themeObject[key] !== "object",
|
||||
)
|
||||
) {
|
||||
throw new Error("PR diff is not a valid theme JSON object.");
|
||||
}
|
||||
|
||||
// Loop through themes and create theme preview body.
|
||||
debug("Create theme preview body...");
|
||||
const themeValid = Object.fromEntries(
|
||||
Object.keys(themeObject).map((name) => [name, true]),
|
||||
);
|
||||
let previewBody = "";
|
||||
for (const theme in themeObject) {
|
||||
debug(`Create theme preview for ${theme}...`);
|
||||
const themeName = theme;
|
||||
const colors = themeObject[theme];
|
||||
const warnings = [];
|
||||
const errors = [];
|
||||
let matches = colorStrings.match(/(title_color:.*bg_color.*\")/);
|
||||
let colors = matches && matches[0].split(",");
|
||||
|
||||
// Check if the theme name is valid.
|
||||
debug("Theme preview body: Check if the theme name is valid...");
|
||||
if (themeNameAlreadyExists(themeName)) {
|
||||
warnings.push("Theme name already taken");
|
||||
themeValid[theme] = false;
|
||||
}
|
||||
if (themeName !== snakeCase(themeName)) {
|
||||
warnings.push("Theme name isn't in snake_case");
|
||||
themeValid[theme] = false;
|
||||
}
|
||||
|
||||
// Check if the theme colors are valid.
|
||||
debug("Theme preview body: Check if the theme colors are valid...");
|
||||
let invalidColors = false;
|
||||
if (colors) {
|
||||
const missingKeys = REQUIRED_COLOR_PROPS.filter(
|
||||
(x) => !Object.keys(colors).includes(x),
|
||||
);
|
||||
const extraKeys = Object.keys(colors).filter(
|
||||
(x) => !ACCEPTED_COLOR_PROPS.includes(x),
|
||||
);
|
||||
if (missingKeys.length > 0 || extraKeys.length > 0) {
|
||||
for (const missingKey of missingKeys) {
|
||||
errors.push(`Theme color properties \`${missingKey}\` are missing`);
|
||||
}
|
||||
|
||||
for (const extraKey of extraKeys) {
|
||||
warnings.push(
|
||||
`Theme color properties \`${extraKey}\` is not supported`,
|
||||
);
|
||||
}
|
||||
invalidColors = true;
|
||||
} else {
|
||||
for (const [colorKey, colorValue] of Object.entries(colors)) {
|
||||
if (colorValue[0] === "#") {
|
||||
errors.push(
|
||||
`Theme color property \`${colorKey}\` should not start with '#'`,
|
||||
);
|
||||
invalidColors = true;
|
||||
} else if (colorValue.length > COLOR_PROPS[colorKey]) {
|
||||
errors.push(
|
||||
`Theme color property \`${colorKey}\` can not be longer than \`${COLOR_PROPS[colorKey]}\` characters`,
|
||||
);
|
||||
invalidColors = true;
|
||||
} else if (
|
||||
!(colorKey === "bg_color" && colorValue.split(",").length > 1
|
||||
? isValidGradient(colorValue.split(","))
|
||||
: isValidHexColor(colorValue))
|
||||
) {
|
||||
errors.push(
|
||||
`Theme color property \`${colorKey}\` is not a valid hex color: <code>${colorValue}</code>`,
|
||||
);
|
||||
invalidColors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warnings.push("Theme colors are missing");
|
||||
invalidColors = true;
|
||||
}
|
||||
if (invalidColors) {
|
||||
themeValid[theme] = false;
|
||||
previewBody += `
|
||||
\r### ${
|
||||
themeName.charAt(0).toUpperCase() + themeName.slice(1)
|
||||
} theme preview
|
||||
|
||||
\r${warnings.map((warning) => `- :warning: ${warning}.\n`).join("")}
|
||||
\r${errors.map((error) => `- :x: ${error}.\n`).join("")}
|
||||
|
||||
\r>:x: Cannot create theme preview.
|
||||
`;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check color contrast.
|
||||
debug("Theme preview body: Check color contrast...");
|
||||
const titleColor = colors.title_color;
|
||||
const iconColor = colors.icon_color;
|
||||
const textColor = colors.text_color;
|
||||
const bgColor = colors.bg_color;
|
||||
const borderColor = colors.border_color;
|
||||
const url = getGRSLink(colors);
|
||||
const colorPairs = {
|
||||
title_color: [titleColor, bgColor],
|
||||
icon_color: [iconColor, bgColor],
|
||||
text_color: [textColor, bgColor],
|
||||
};
|
||||
Object.keys(colorPairs).forEach((item) => {
|
||||
let color1 = colorPairs[item][0];
|
||||
let color2 = colorPairs[item][1];
|
||||
const isGradientColor = color2.split(",").length > 1;
|
||||
if (isGradientColor) {
|
||||
return;
|
||||
}
|
||||
color1 = color1.length === 4 ? color1.slice(0, 3) : color1.slice(0, 6);
|
||||
color2 = color2.length === 4 ? color2.slice(0, 3) : color2.slice(0, 6);
|
||||
if (!ccc.isLevelAA(`#${color1}`, `#${color2}`)) {
|
||||
const permalink = getWebAimLink(color1, color2);
|
||||
warnings.push(
|
||||
`\`${item}\` does not pass [AA contrast ratio](${permalink})`,
|
||||
);
|
||||
themeValid[theme] = false;
|
||||
}
|
||||
if (!colors) {
|
||||
await octokit.issues.createComment({
|
||||
owner: "anuraghazra",
|
||||
repo: "github-readme-stats",
|
||||
body: `
|
||||
\rTheme preview (bot)
|
||||
Cannot create theme preview
|
||||
`,
|
||||
issue_number: pullRequestId,
|
||||
});
|
||||
|
||||
// Create theme preview body.
|
||||
debug("Theme preview body: Create theme preview body...");
|
||||
previewBody += `
|
||||
\r### ${
|
||||
themeName.charAt(0).toUpperCase() + themeName.slice(1)
|
||||
} theme preview
|
||||
|
||||
\r${warnings.map((warning) => `- :warning: ${warning}.\n`).join("")}
|
||||
|
||||
\ntitle_color: <code>#${titleColor}</code> | icon_color: <code>#${iconColor}</code> | text_color: <code>#${textColor}</code> | bg_color: <code>#${bgColor}</code>${
|
||||
borderColor ? ` | border_color: <code>#${borderColor}</code>` : ""
|
||||
}
|
||||
|
||||
\r[Preview Link](${url})
|
||||
|
||||
\r[](${url})
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Create comment body.
|
||||
debug("Create comment body...");
|
||||
commentBody += `
|
||||
\r${
|
||||
Object.values(themeValid).every((value) => value)
|
||||
? THEME_PR_SUCCESS_TEXT
|
||||
: THEME_PR_FAIL_TEXT
|
||||
}
|
||||
\r## Test results
|
||||
\r${Object.entries(themeValid)
|
||||
.map(
|
||||
([key, value]) => `- ${value ? ":heavy_check_mark:" : ":x:"} ${key}`,
|
||||
)
|
||||
.join("\r")}
|
||||
|
||||
\r${
|
||||
Object.values(themeValid).every((value) => value)
|
||||
? "**Result:** :heavy_check_mark: All themes are valid."
|
||||
: "**Result:** :x: Some themes are invalid.\n\n" + FAIL_TEXT
|
||||
}
|
||||
|
||||
\r## Details
|
||||
\r${previewBody}
|
||||
`;
|
||||
|
||||
// Create or update theme-preview comment.
|
||||
debug("Create or update theme-preview comment...");
|
||||
let comment_url;
|
||||
if (DRY_RUN) {
|
||||
info(`DRY_RUN: Comment body: ${commentBody}`);
|
||||
comment_url = "";
|
||||
} else {
|
||||
comment_url = await upsertComment(
|
||||
OCTOKIT,
|
||||
PULL_REQUEST_ID,
|
||||
REPO,
|
||||
OWNER,
|
||||
comment?.id,
|
||||
commentBody,
|
||||
);
|
||||
}
|
||||
|
||||
// Change review state and add/remove `invalid` label based on theme PR validity.
|
||||
debug(
|
||||
"Change review state and add/remove `invalid` label based on whether all themes passed...",
|
||||
colors = colors.map((color) =>
|
||||
color.replace(/.*\:\s/, "").replace(/\"/g, "")
|
||||
);
|
||||
const themesValid = Object.values(themeValid).every((value) => value);
|
||||
const reviewState = themesValid ? "APPROVE" : "REQUEST_CHANGES";
|
||||
const reviewReason = themesValid
|
||||
? undefined
|
||||
: INVALID_REVIEW_COMMENT(comment_url);
|
||||
if (DRY_RUN) {
|
||||
info(`DRY_RUN: Review state: ${reviewState}`);
|
||||
info(`DRY_RUN: Review reason: ${reviewReason}`);
|
||||
} else {
|
||||
await addReview(
|
||||
OCTOKIT,
|
||||
PULL_REQUEST_ID,
|
||||
OWNER,
|
||||
REPO,
|
||||
reviewState,
|
||||
reviewReason,
|
||||
);
|
||||
await addRemoveLabel(
|
||||
OCTOKIT,
|
||||
PULL_REQUEST_ID,
|
||||
OWNER,
|
||||
REPO,
|
||||
"invalid",
|
||||
!themesValid,
|
||||
);
|
||||
}
|
||||
|
||||
let titleColor = colors[0];
|
||||
let iconColor = colors[1];
|
||||
let textColor = colors[2];
|
||||
let bgColor = colors[3];
|
||||
const url = `https://github-readme-stats.vercel.app/api?username=anuraghazra&title_color=${titleColor}&icon_color=${iconColor}&text_color=${textColor}&bg_color=${bgColor}&show_icons=true`;
|
||||
|
||||
await octokit.issues.createComment({
|
||||
owner: "anuraghazra",
|
||||
repo: "github-readme-stats",
|
||||
body: `
|
||||
\rTheme preview (bot)
|
||||
\r
|
||||
`,
|
||||
issue_number: pullRequestId,
|
||||
});
|
||||
} catch (error) {
|
||||
debug("Set review state to `REQUEST_CHANGES` and add `invalid` label...");
|
||||
if (DRY_RUN) {
|
||||
info(`DRY_RUN: Review state: REQUEST_CHANGES`);
|
||||
info(`DRY_RUN: Review reason: ${error.message}`);
|
||||
} else {
|
||||
await addReview(
|
||||
OCTOKIT,
|
||||
PULL_REQUEST_ID,
|
||||
OWNER,
|
||||
REPO,
|
||||
"REQUEST_CHANGES",
|
||||
"**Something went wrong in the theme preview action:** `" +
|
||||
error.message +
|
||||
"`",
|
||||
);
|
||||
await addRemoveLabel(
|
||||
OCTOKIT,
|
||||
PULL_REQUEST_ID,
|
||||
OWNER,
|
||||
REPO,
|
||||
"invalid",
|
||||
true,
|
||||
);
|
||||
}
|
||||
setFailed(error.message);
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
@@ -5,11 +5,10 @@ set -e
|
||||
export BRANCH_NAME=updated-theme-readme
|
||||
git --version
|
||||
git config --global user.email "no-reply@githubreadmestats.com"
|
||||
git config --global user.name "GitHub Readme Stats Bot"
|
||||
git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||
git config --global user.name "Github Readme Stats Bot"
|
||||
git branch -d $BRANCH_NAME || true
|
||||
git checkout -b $BRANCH_NAME
|
||||
git add --all
|
||||
git commit --no-verify --message "docs(theme): auto update theme readme"
|
||||
git commit --message "docs(theme): Auto update theme readme" || exit 0
|
||||
git remote add origin-$BRANCH_NAME https://${PERSONAL_TOKEN}@github.com/${GH_REPO}.git
|
||||
git push --force --quiet --set-upstream origin-$BRANCH_NAME $BRANCH_NAME
|
||||
git push --force --quiet --set-upstream origin-$BRANCH_NAME $BRANCH_NAME
|
||||
+80
-74
@@ -1,87 +1,93 @@
|
||||
/**
|
||||
* Calculates the exponential cdf.
|
||||
*
|
||||
* @param {number} x The value.
|
||||
* @returns {number} The exponential cdf.
|
||||
*/
|
||||
function exponential_cdf(x) {
|
||||
return 1 - 2 ** -x;
|
||||
// https://stackoverflow.com/a/5263759/10629172
|
||||
function normalcdf(mean, sigma, to) {
|
||||
var z = (to - mean) / Math.sqrt(2 * sigma * sigma);
|
||||
var t = 1 / (1 + 0.3275911 * Math.abs(z));
|
||||
var a1 = 0.254829592;
|
||||
var a2 = -0.284496736;
|
||||
var a3 = 1.421413741;
|
||||
var a4 = -1.453152027;
|
||||
var a5 = 1.061405429;
|
||||
var erf =
|
||||
1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-z * z);
|
||||
var sign = 1;
|
||||
if (z < 0) {
|
||||
sign = -1;
|
||||
}
|
||||
return (1 / 2) * (1 + sign * erf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the log normal cdf.
|
||||
*
|
||||
* @param {number} x The value.
|
||||
* @returns {number} The log normal cdf.
|
||||
*/
|
||||
function log_normal_cdf(x) {
|
||||
// approximation
|
||||
return x / (1 + x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the users rank.
|
||||
*
|
||||
* @param {object} params Parameters on which the user's rank depends.
|
||||
* @param {boolean} params.all_commits Whether `include_all_commits` was used.
|
||||
* @param {number} params.commits Number of commits.
|
||||
* @param {number} params.prs The number of pull requests.
|
||||
* @param {number} params.issues The number of issues.
|
||||
* @param {number} params.reviews The number of reviews.
|
||||
* @param {number} params.repos Total number of repos.
|
||||
* @param {number} params.stars The number of stars.
|
||||
* @param {number} params.followers The number of followers.
|
||||
* @returns {{ level: string, percentile: number }} The users rank.
|
||||
*/
|
||||
function calculateRank({
|
||||
all_commits,
|
||||
commits,
|
||||
totalRepos,
|
||||
totalCommits,
|
||||
contributions,
|
||||
followers,
|
||||
prs,
|
||||
issues,
|
||||
reviews,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
repos, // unused
|
||||
stars,
|
||||
followers,
|
||||
stargazers,
|
||||
}) {
|
||||
const COMMITS_MEDIAN = all_commits ? 1000 : 250,
|
||||
COMMITS_WEIGHT = 2;
|
||||
const PRS_MEDIAN = 50,
|
||||
PRS_WEIGHT = 3;
|
||||
const ISSUES_MEDIAN = 25,
|
||||
ISSUES_WEIGHT = 1;
|
||||
const REVIEWS_MEDIAN = 2,
|
||||
REVIEWS_WEIGHT = 1;
|
||||
const STARS_MEDIAN = 50,
|
||||
STARS_WEIGHT = 4;
|
||||
const FOLLOWERS_MEDIAN = 10,
|
||||
FOLLOWERS_WEIGHT = 1;
|
||||
const COMMITS_OFFSET = 1.65;
|
||||
const CONTRIBS_OFFSET = 1.65;
|
||||
const ISSUES_OFFSET = 1;
|
||||
const STARS_OFFSET = 0.75;
|
||||
const PRS_OFFSET = 0.5;
|
||||
const FOLLOWERS_OFFSET = 0.45;
|
||||
const REPO_OFFSET = 1;
|
||||
|
||||
const TOTAL_WEIGHT =
|
||||
COMMITS_WEIGHT +
|
||||
PRS_WEIGHT +
|
||||
ISSUES_WEIGHT +
|
||||
REVIEWS_WEIGHT +
|
||||
STARS_WEIGHT +
|
||||
FOLLOWERS_WEIGHT;
|
||||
const ALL_OFFSETS =
|
||||
CONTRIBS_OFFSET +
|
||||
ISSUES_OFFSET +
|
||||
STARS_OFFSET +
|
||||
PRS_OFFSET +
|
||||
FOLLOWERS_OFFSET +
|
||||
REPO_OFFSET;
|
||||
|
||||
const THRESHOLDS = [1, 12.5, 25, 37.5, 50, 62.5, 75, 87.5, 100];
|
||||
const LEVELS = ["S", "A+", "A", "A-", "B+", "B", "B-", "C+", "C"];
|
||||
const RANK_S_VALUE = 1;
|
||||
const RANK_DOUBLE_A_VALUE = 25;
|
||||
const RANK_A2_VALUE = 45;
|
||||
const RANK_A3_VALUE = 60;
|
||||
const RANK_B_VALUE = 100;
|
||||
|
||||
const rank =
|
||||
1 -
|
||||
(COMMITS_WEIGHT * exponential_cdf(commits / COMMITS_MEDIAN) +
|
||||
PRS_WEIGHT * exponential_cdf(prs / PRS_MEDIAN) +
|
||||
ISSUES_WEIGHT * exponential_cdf(issues / ISSUES_MEDIAN) +
|
||||
REVIEWS_WEIGHT * exponential_cdf(reviews / REVIEWS_MEDIAN) +
|
||||
STARS_WEIGHT * log_normal_cdf(stars / STARS_MEDIAN) +
|
||||
FOLLOWERS_WEIGHT * log_normal_cdf(followers / FOLLOWERS_MEDIAN)) /
|
||||
TOTAL_WEIGHT;
|
||||
const TOTAL_VALUES =
|
||||
RANK_S_VALUE + RANK_A2_VALUE + RANK_A3_VALUE + RANK_B_VALUE;
|
||||
|
||||
const level = LEVELS[THRESHOLDS.findIndex((t) => rank * 100 <= t)];
|
||||
// prettier-ignore
|
||||
const score = (
|
||||
totalCommits * COMMITS_OFFSET +
|
||||
contributions * CONTRIBS_OFFSET +
|
||||
issues * ISSUES_OFFSET +
|
||||
stargazers * STARS_OFFSET +
|
||||
prs * PRS_OFFSET +
|
||||
followers * FOLLOWERS_OFFSET +
|
||||
totalRepos * REPO_OFFSET
|
||||
) / 100;
|
||||
|
||||
return { level, percentile: rank * 100 };
|
||||
const normalizedScore = normalcdf(score, TOTAL_VALUES, ALL_OFFSETS) * 100;
|
||||
|
||||
let level = "";
|
||||
|
||||
if (normalizedScore < RANK_S_VALUE) {
|
||||
level = "S+";
|
||||
}
|
||||
if (
|
||||
normalizedScore >= RANK_S_VALUE &&
|
||||
normalizedScore < RANK_DOUBLE_A_VALUE
|
||||
) {
|
||||
level = "S";
|
||||
}
|
||||
if (
|
||||
normalizedScore >= RANK_DOUBLE_A_VALUE &&
|
||||
normalizedScore < RANK_A2_VALUE
|
||||
) {
|
||||
level = "A++";
|
||||
}
|
||||
if (normalizedScore >= RANK_A2_VALUE && normalizedScore < RANK_A3_VALUE) {
|
||||
level = "A+";
|
||||
}
|
||||
if (normalizedScore >= RANK_A3_VALUE && normalizedScore < RANK_B_VALUE) {
|
||||
level = "B+";
|
||||
}
|
||||
|
||||
return { level, score: normalizedScore };
|
||||
}
|
||||
|
||||
export { calculateRank };
|
||||
export default calculateRank;
|
||||
module.exports = calculateRank;
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import {
|
||||
measureText,
|
||||
flexLayout,
|
||||
iconWithLabel,
|
||||
createLanguageNode,
|
||||
} from "../common/render.js";
|
||||
import Card from "../common/Card.js";
|
||||
import { getCardColors } from "../common/color.js";
|
||||
import { kFormatter, wrapTextMultiline } from "../common/fmt.js";
|
||||
import { encodeHTML } from "../common/html.js";
|
||||
import { icons } from "../common/icons.js";
|
||||
import { parseEmojis } from "../common/ops.js";
|
||||
|
||||
/** Import language colors.
|
||||
*
|
||||
* @description Here we use the workaround found in
|
||||
* https://stackoverflow.com/questions/66726365/how-should-i-import-json-in-node
|
||||
* since vercel is using v16.14.0 which does not yet support json imports without the
|
||||
* --experimental-json-modules flag.
|
||||
*/
|
||||
import { createRequire } from "module";
|
||||
const require = createRequire(import.meta.url);
|
||||
const languageColors = require("../common/languageColors.json"); // now works
|
||||
|
||||
const ICON_SIZE = 16;
|
||||
const CARD_DEFAULT_WIDTH = 400;
|
||||
const HEADER_MAX_LENGTH = 35;
|
||||
|
||||
/**
|
||||
* @typedef {import('./types').GistCardOptions} GistCardOptions Gist card options.
|
||||
* @typedef {import('../fetchers/types').GistData} GistData Gist data.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Render gist card.
|
||||
*
|
||||
* @param {GistData} gistData Gist data.
|
||||
* @param {Partial<GistCardOptions>} options Gist card options.
|
||||
* @returns {string} Gist card.
|
||||
*/
|
||||
const renderGistCard = (gistData, options = {}) => {
|
||||
const { name, nameWithOwner, description, language, starsCount, forksCount } =
|
||||
gistData;
|
||||
const {
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
border_radius,
|
||||
border_color,
|
||||
show_owner = false,
|
||||
hide_border = false,
|
||||
} = options;
|
||||
|
||||
// returns theme based colors with proper overrides and defaults
|
||||
const { titleColor, textColor, iconColor, bgColor, borderColor } =
|
||||
getCardColors({
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
});
|
||||
|
||||
const lineWidth = 59;
|
||||
const linesLimit = 10;
|
||||
const desc = parseEmojis(description || "No description provided");
|
||||
const multiLineDescription = wrapTextMultiline(desc, lineWidth, linesLimit);
|
||||
const descriptionLines = multiLineDescription.length;
|
||||
const descriptionSvg = multiLineDescription
|
||||
.map((line) => `<tspan dy="1.2em" x="25">${encodeHTML(line)}</tspan>`)
|
||||
.join("");
|
||||
|
||||
const lineHeight = descriptionLines > 3 ? 12 : 10;
|
||||
const height =
|
||||
(descriptionLines > 1 ? 120 : 110) + descriptionLines * lineHeight;
|
||||
|
||||
const totalStars = kFormatter(starsCount);
|
||||
const totalForks = kFormatter(forksCount);
|
||||
const svgStars = iconWithLabel(
|
||||
icons.star,
|
||||
totalStars,
|
||||
"starsCount",
|
||||
ICON_SIZE,
|
||||
);
|
||||
const svgForks = iconWithLabel(
|
||||
icons.fork,
|
||||
totalForks,
|
||||
"forksCount",
|
||||
ICON_SIZE,
|
||||
);
|
||||
|
||||
const languageName = language || "Unspecified";
|
||||
// @ts-ignore
|
||||
const languageColor = languageColors[languageName] || "#858585";
|
||||
|
||||
const svgLanguage = createLanguageNode(languageName, languageColor);
|
||||
|
||||
const starAndForkCount = flexLayout({
|
||||
items: [svgLanguage, svgStars, svgForks],
|
||||
sizes: [
|
||||
measureText(languageName, 12),
|
||||
ICON_SIZE + measureText(`${totalStars}`, 12),
|
||||
ICON_SIZE + measureText(`${totalForks}`, 12),
|
||||
],
|
||||
gap: 25,
|
||||
}).join("");
|
||||
|
||||
const header = show_owner ? nameWithOwner : name;
|
||||
|
||||
const card = new Card({
|
||||
defaultTitle:
|
||||
header.length > HEADER_MAX_LENGTH
|
||||
? `${header.slice(0, HEADER_MAX_LENGTH)}...`
|
||||
: header,
|
||||
titlePrefixIcon: icons.gist,
|
||||
width: CARD_DEFAULT_WIDTH,
|
||||
height,
|
||||
border_radius,
|
||||
colors: {
|
||||
titleColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
bgColor,
|
||||
borderColor,
|
||||
},
|
||||
});
|
||||
|
||||
card.setCSS(`
|
||||
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
|
||||
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
|
||||
.icon { fill: ${iconColor} }
|
||||
`);
|
||||
card.setHideBorder(hide_border);
|
||||
|
||||
return card.render(`
|
||||
<text class="description" x="25" y="-5">
|
||||
${descriptionSvg}
|
||||
</text>
|
||||
|
||||
<g transform="translate(30, ${height - 75})">
|
||||
${starAndForkCount}
|
||||
</g>
|
||||
`);
|
||||
};
|
||||
|
||||
export { renderGistCard, HEADER_MAX_LENGTH };
|
||||
export default renderGistCard;
|
||||
@@ -1,4 +0,0 @@
|
||||
export { renderRepoCard } from "./repo.js";
|
||||
export { renderStatsCard } from "./stats.js";
|
||||
export { renderTopLanguages } from "./top-languages.js";
|
||||
export { renderWakatimeCard } from "./wakatime.js";
|
||||
@@ -0,0 +1,159 @@
|
||||
const {
|
||||
kFormatter,
|
||||
encodeHTML,
|
||||
getCardColors,
|
||||
FlexLayout,
|
||||
wrapTextMultiline,
|
||||
} = require("../common/utils");
|
||||
const icons = require("../common/icons");
|
||||
const Card = require("../common/Card");
|
||||
const toEmoji = require("emoji-name-map");
|
||||
|
||||
const renderRepoCard = (repo, options = {}) => {
|
||||
const {
|
||||
name,
|
||||
nameWithOwner,
|
||||
description,
|
||||
primaryLanguage,
|
||||
stargazers,
|
||||
isArchived,
|
||||
isTemplate,
|
||||
forkCount,
|
||||
} = repo;
|
||||
const {
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
show_owner,
|
||||
theme = "default_repocard",
|
||||
} = options;
|
||||
|
||||
const header = show_owner ? nameWithOwner : name;
|
||||
const langName = (primaryLanguage && primaryLanguage.name) || "Unspecified";
|
||||
const langColor = (primaryLanguage && primaryLanguage.color) || "#333";
|
||||
|
||||
const shiftText = langName.length > 15 ? 0 : 30;
|
||||
|
||||
let desc = description || "No description provided";
|
||||
|
||||
// parse emojis to unicode
|
||||
desc = desc.replace(/:\w+:/gm, (emoji) => {
|
||||
return toEmoji.get(emoji) || "";
|
||||
});
|
||||
|
||||
const multiLineDescription = wrapTextMultiline(desc);
|
||||
const descriptionLines = multiLineDescription.length;
|
||||
const lineHeight = 10;
|
||||
|
||||
const height =
|
||||
(descriptionLines > 1 ? 120 : 110) + descriptionLines * lineHeight;
|
||||
|
||||
// returns theme based colors with proper overrides and defaults
|
||||
const { titleColor, textColor, iconColor, bgColor } = getCardColors({
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
});
|
||||
|
||||
const totalStars = kFormatter(stargazers.totalCount);
|
||||
const totalForks = kFormatter(forkCount);
|
||||
|
||||
const getBadgeSVG = (label) => `
|
||||
<g data-testid="badge" class="badge" transform="translate(320, 38)">
|
||||
<rect stroke="${textColor}" stroke-width="1" width="70" height="20" x="-12" y="-14" ry="10" rx="10"></rect>
|
||||
<text
|
||||
x="23" y="-5"
|
||||
alignment-baseline="central"
|
||||
dominant-baseline="central"
|
||||
text-anchor="middle"
|
||||
fill="${textColor}"
|
||||
>
|
||||
${label}
|
||||
</text>
|
||||
</g>
|
||||
`;
|
||||
|
||||
const svgLanguage = primaryLanguage
|
||||
? `
|
||||
<g data-testid="primary-lang" transform="translate(30, 0)">
|
||||
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="${langColor}" />
|
||||
<text data-testid="lang-name" class="gray" x="15">${langName}</text>
|
||||
</g>
|
||||
`
|
||||
: "";
|
||||
|
||||
const iconWithLabel = (icon, label, testid) => {
|
||||
return `
|
||||
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
${icon}
|
||||
</svg>
|
||||
<text data-testid="${testid}" class="gray" x="25">${label}</text>
|
||||
`;
|
||||
};
|
||||
const svgStars =
|
||||
stargazers.totalCount > 0 &&
|
||||
iconWithLabel(icons.star, totalStars, "stargazers");
|
||||
const svgForks =
|
||||
forkCount > 0 && iconWithLabel(icons.fork, totalForks, "forkcount");
|
||||
|
||||
const starAndForkCount = FlexLayout({
|
||||
items: [svgStars, svgForks],
|
||||
gap: 65,
|
||||
}).join("");
|
||||
|
||||
const card = new Card({
|
||||
title: header,
|
||||
titlePrefixIcon: icons.contribs,
|
||||
width: 400,
|
||||
height,
|
||||
colors: {
|
||||
titleColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
bgColor,
|
||||
},
|
||||
});
|
||||
|
||||
card.disableAnimations();
|
||||
card.setHideBorder(false);
|
||||
card.setHideTitle(false);
|
||||
card.setCSS(`
|
||||
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
|
||||
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
|
||||
.icon { fill: ${iconColor} }
|
||||
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
|
||||
.badge rect { opacity: 0.2 }
|
||||
`);
|
||||
|
||||
return card.render(`
|
||||
${
|
||||
isTemplate
|
||||
? getBadgeSVG("Template")
|
||||
: isArchived
|
||||
? getBadgeSVG("Archived")
|
||||
: ""
|
||||
}
|
||||
|
||||
<text class="description" x="25" y="-5">
|
||||
${multiLineDescription
|
||||
.map((line) => `<tspan dy="1.2em" x="25">${encodeHTML(line)}</tspan>`)
|
||||
.join("")}
|
||||
</text>
|
||||
|
||||
<g transform="translate(0, ${height - 75})">
|
||||
${svgLanguage}
|
||||
|
||||
<g
|
||||
data-testid="star-fork-group"
|
||||
transform="translate(${primaryLanguage ? 155 - shiftText : 25}, 0)"
|
||||
>
|
||||
${starAndForkCount}
|
||||
</g>
|
||||
</g>
|
||||
`);
|
||||
};
|
||||
|
||||
module.exports = renderRepoCard;
|
||||
@@ -1,193 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import { Card } from "../common/Card.js";
|
||||
import { getCardColors } from "../common/color.js";
|
||||
import { kFormatter, wrapTextMultiline } from "../common/fmt.js";
|
||||
import { encodeHTML } from "../common/html.js";
|
||||
import { I18n } from "../common/I18n.js";
|
||||
import { icons } from "../common/icons.js";
|
||||
import { clampValue, parseEmojis } from "../common/ops.js";
|
||||
import {
|
||||
flexLayout,
|
||||
measureText,
|
||||
iconWithLabel,
|
||||
createLanguageNode,
|
||||
} from "../common/render.js";
|
||||
import { repoCardLocales } from "../translations.js";
|
||||
|
||||
const ICON_SIZE = 16;
|
||||
const DESCRIPTION_LINE_WIDTH = 59;
|
||||
const DESCRIPTION_MAX_LINES = 3;
|
||||
|
||||
/**
|
||||
* Retrieves the repository description and wraps it to fit the card width.
|
||||
*
|
||||
* @param {string} label The repository description.
|
||||
* @param {string} textColor The color of the text.
|
||||
* @returns {string} Wrapped repo description SVG object.
|
||||
*/
|
||||
const getBadgeSVG = (label, textColor) => `
|
||||
<g data-testid="badge" class="badge" transform="translate(320, -18)">
|
||||
<rect stroke="${textColor}" stroke-width="1" width="70" height="20" x="-12" y="-14" ry="10" rx="10"></rect>
|
||||
<text
|
||||
x="23" y="-5"
|
||||
alignment-baseline="central"
|
||||
dominant-baseline="central"
|
||||
text-anchor="middle"
|
||||
fill="${textColor}"
|
||||
>
|
||||
${label}
|
||||
</text>
|
||||
</g>
|
||||
`;
|
||||
|
||||
/**
|
||||
* @typedef {import("../fetchers/types").RepositoryData} RepositoryData Repository data.
|
||||
* @typedef {import("./types").RepoCardOptions} RepoCardOptions Repo card options.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Renders repository card details.
|
||||
*
|
||||
* @param {RepositoryData} repo Repository data.
|
||||
* @param {Partial<RepoCardOptions>} options Card options.
|
||||
* @returns {string} Repository card SVG object.
|
||||
*/
|
||||
const renderRepoCard = (repo, options = {}) => {
|
||||
const {
|
||||
name,
|
||||
nameWithOwner,
|
||||
description,
|
||||
primaryLanguage,
|
||||
isArchived,
|
||||
isTemplate,
|
||||
starCount,
|
||||
forkCount,
|
||||
} = repo;
|
||||
const {
|
||||
hide_border = false,
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
show_owner = false,
|
||||
theme = "default_repocard",
|
||||
border_radius,
|
||||
border_color,
|
||||
locale,
|
||||
description_lines_count,
|
||||
} = options;
|
||||
|
||||
const lineHeight = 10;
|
||||
const header = show_owner ? nameWithOwner : name;
|
||||
const langName = (primaryLanguage && primaryLanguage.name) || "Unspecified";
|
||||
const langColor = (primaryLanguage && primaryLanguage.color) || "#333";
|
||||
const descriptionMaxLines = description_lines_count
|
||||
? clampValue(description_lines_count, 1, DESCRIPTION_MAX_LINES)
|
||||
: DESCRIPTION_MAX_LINES;
|
||||
|
||||
const desc = parseEmojis(description || "No description provided");
|
||||
const multiLineDescription = wrapTextMultiline(
|
||||
desc,
|
||||
DESCRIPTION_LINE_WIDTH,
|
||||
descriptionMaxLines,
|
||||
);
|
||||
const descriptionLinesCount = description_lines_count
|
||||
? clampValue(description_lines_count, 1, DESCRIPTION_MAX_LINES)
|
||||
: multiLineDescription.length;
|
||||
|
||||
const descriptionSvg = multiLineDescription
|
||||
.map((line) => `<tspan dy="1.2em" x="25">${encodeHTML(line)}</tspan>`)
|
||||
.join("");
|
||||
|
||||
const height =
|
||||
(descriptionLinesCount > 1 ? 120 : 110) +
|
||||
descriptionLinesCount * lineHeight;
|
||||
|
||||
const i18n = new I18n({
|
||||
locale,
|
||||
translations: repoCardLocales,
|
||||
});
|
||||
|
||||
// returns theme based colors with proper overrides and defaults
|
||||
const colors = getCardColors({
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
});
|
||||
|
||||
const svgLanguage = primaryLanguage
|
||||
? createLanguageNode(langName, langColor)
|
||||
: "";
|
||||
|
||||
const totalStars = kFormatter(starCount);
|
||||
const totalForks = kFormatter(forkCount);
|
||||
const svgStars = iconWithLabel(
|
||||
icons.star,
|
||||
totalStars,
|
||||
"stargazers",
|
||||
ICON_SIZE,
|
||||
);
|
||||
const svgForks = iconWithLabel(
|
||||
icons.fork,
|
||||
totalForks,
|
||||
"forkcount",
|
||||
ICON_SIZE,
|
||||
);
|
||||
|
||||
const starAndForkCount = flexLayout({
|
||||
items: [svgLanguage, svgStars, svgForks],
|
||||
sizes: [
|
||||
measureText(langName, 12),
|
||||
ICON_SIZE + measureText(`${totalStars}`, 12),
|
||||
ICON_SIZE + measureText(`${totalForks}`, 12),
|
||||
],
|
||||
gap: 25,
|
||||
}).join("");
|
||||
|
||||
const card = new Card({
|
||||
defaultTitle: header.length > 35 ? `${header.slice(0, 35)}...` : header,
|
||||
titlePrefixIcon: icons.contribs,
|
||||
width: 400,
|
||||
height,
|
||||
border_radius,
|
||||
colors,
|
||||
});
|
||||
|
||||
card.disableAnimations();
|
||||
card.setHideBorder(hide_border);
|
||||
card.setHideTitle(false);
|
||||
card.setCSS(`
|
||||
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.textColor} }
|
||||
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.textColor} }
|
||||
.icon { fill: ${colors.iconColor} }
|
||||
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
|
||||
.badge rect { opacity: 0.2 }
|
||||
`);
|
||||
|
||||
return card.render(`
|
||||
${
|
||||
isTemplate
|
||||
? // @ts-ignore
|
||||
getBadgeSVG(i18n.t("repocard.template"), colors.textColor)
|
||||
: isArchived
|
||||
? // @ts-ignore
|
||||
getBadgeSVG(i18n.t("repocard.archived"), colors.textColor)
|
||||
: ""
|
||||
}
|
||||
|
||||
<text class="description" x="25" y="-5">
|
||||
${descriptionSvg}
|
||||
</text>
|
||||
|
||||
<g transform="translate(30, ${height - 75})">
|
||||
${starAndForkCount}
|
||||
</g>
|
||||
`);
|
||||
};
|
||||
|
||||
export { renderRepoCard };
|
||||
export default renderRepoCard;
|
||||
@@ -0,0 +1,208 @@
|
||||
const {
|
||||
kFormatter,
|
||||
getCardColors,
|
||||
FlexLayout,
|
||||
encodeHTML,
|
||||
} = require("../common/utils");
|
||||
const { getStyles } = require("../getStyles");
|
||||
const icons = require("../common/icons");
|
||||
const Card = require("../common/Card");
|
||||
|
||||
const createTextNode = ({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
id,
|
||||
index,
|
||||
showIcons,
|
||||
shiftValuePos,
|
||||
}) => {
|
||||
const kValue = kFormatter(value);
|
||||
const staggerDelay = (index + 3) * 150;
|
||||
|
||||
const labelOffset = showIcons ? `x="25"` : "";
|
||||
const iconSvg = showIcons
|
||||
? `
|
||||
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
${icon}
|
||||
</svg>
|
||||
`
|
||||
: "";
|
||||
return `
|
||||
<g class="stagger" style="animation-delay: ${staggerDelay}ms" transform="translate(25, 0)">
|
||||
${iconSvg}
|
||||
<text class="stat bold" ${labelOffset} y="12.5">${label}:</text>
|
||||
<text
|
||||
class="stat"
|
||||
x="${shiftValuePos ? (showIcons ? 200 : 170) : 150}"
|
||||
y="12.5"
|
||||
data-testid="${id}"
|
||||
>${kValue}</text>
|
||||
</g>
|
||||
`;
|
||||
};
|
||||
|
||||
const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
const {
|
||||
name,
|
||||
totalStars,
|
||||
totalCommits,
|
||||
totalIssues,
|
||||
totalPRs,
|
||||
contributedTo,
|
||||
rank,
|
||||
totalForkRepositories,
|
||||
} = stats;
|
||||
const {
|
||||
hide = [],
|
||||
show_stats = [],
|
||||
show_icons = false,
|
||||
hide_title = false,
|
||||
hide_border = false,
|
||||
hide_rank = false,
|
||||
include_all_commits = false,
|
||||
line_height = 25,
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme = "default",
|
||||
} = options;
|
||||
|
||||
const new_hide = hide.concat(["forks"]);
|
||||
const lheight = parseInt(line_height, 10);
|
||||
|
||||
// returns theme based colors with proper overrides and defaults
|
||||
const { titleColor, textColor, iconColor, bgColor } = getCardColors({
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
});
|
||||
|
||||
// Meta data for creating text nodes with createTextNode function
|
||||
const STATS = {
|
||||
stars: {
|
||||
icon: icons.star,
|
||||
label: "Total Stars",
|
||||
value: totalStars,
|
||||
id: "stars",
|
||||
},
|
||||
commits: {
|
||||
icon: icons.commits,
|
||||
label: `Total Commits${
|
||||
include_all_commits ? "" : ` (${new Date().getFullYear()})`
|
||||
}`,
|
||||
value: totalCommits,
|
||||
id: "commits",
|
||||
},
|
||||
prs: {
|
||||
icon: icons.prs,
|
||||
label: "Total PRs",
|
||||
value: totalPRs,
|
||||
id: "prs",
|
||||
},
|
||||
issues: {
|
||||
icon: icons.issues,
|
||||
label: "Total Issues",
|
||||
value: totalIssues,
|
||||
id: "issues",
|
||||
},
|
||||
contribs: {
|
||||
icon: icons.contribs,
|
||||
label: "Contributed to",
|
||||
value: contributedTo,
|
||||
id: "contribs",
|
||||
},
|
||||
forks: {
|
||||
icon: icons.fork,
|
||||
label: "Total Forks",
|
||||
value: totalForkRepositories,
|
||||
id: "forks",
|
||||
},
|
||||
};
|
||||
|
||||
// filter out hidden stats defined by user & create the text nodes
|
||||
const statItems = Object.keys(STATS)
|
||||
.filter((key) => !new_hide.includes(key))
|
||||
.concat(show_stats)
|
||||
.map((key, index) =>
|
||||
// create the text nodes, and pass index so that we can calculate the line spacing
|
||||
createTextNode({
|
||||
...STATS[key],
|
||||
index,
|
||||
showIcons: show_icons,
|
||||
shiftValuePos: !include_all_commits,
|
||||
})
|
||||
);
|
||||
|
||||
// Calculate the card height depending on how many items there are
|
||||
// but if rank circle is visible clamp the minimum height to `150`
|
||||
let height = Math.max(
|
||||
45 + (statItems.length + 1) * lheight,
|
||||
hide_rank ? 0 : 150
|
||||
);
|
||||
|
||||
// Conditionally rendered elements
|
||||
const rankCircle = hide_rank
|
||||
? ""
|
||||
: `<g data-testid="rank-circle"
|
||||
transform="translate(400, ${height / 2 - 50})">
|
||||
<circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
|
||||
<circle class="rank-circle" cx="-10" cy="8" r="40" />
|
||||
<g class="rank-text">
|
||||
<text
|
||||
x="${rank.level.length === 1 ? "-4" : "0"}"
|
||||
y="0"
|
||||
alignment-baseline="central"
|
||||
dominant-baseline="central"
|
||||
text-anchor="middle"
|
||||
>
|
||||
${rank.level}
|
||||
</text>
|
||||
</g>
|
||||
</g>`;
|
||||
|
||||
// the better user's score the the rank will be closer to zero so
|
||||
// subtracting 100 to get the progress in 100%
|
||||
const progress = 100 - rank.score;
|
||||
const cssStyles = getStyles({
|
||||
titleColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
show_icons,
|
||||
progress,
|
||||
});
|
||||
|
||||
const apostrophe = ["x", "s"].includes(name.slice(-1)) ? "" : "s";
|
||||
const card = new Card({
|
||||
title: `${encodeHTML(name)}'${apostrophe} GitHub Stats`,
|
||||
width: 495,
|
||||
height,
|
||||
colors: {
|
||||
titleColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
bgColor,
|
||||
},
|
||||
});
|
||||
|
||||
card.setHideBorder(hide_border);
|
||||
card.setHideTitle(hide_title);
|
||||
card.setCSS(cssStyles);
|
||||
|
||||
return card.render(`
|
||||
${rankCircle}
|
||||
|
||||
<svg x="0" y="0">
|
||||
${FlexLayout({
|
||||
items: statItems,
|
||||
gap: lheight,
|
||||
direction: "column",
|
||||
}).join("")}
|
||||
</svg>
|
||||
`);
|
||||
};
|
||||
|
||||
module.exports = renderStatsCard;
|
||||
@@ -1,602 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import { Card } from "../common/Card.js";
|
||||
import { getCardColors } from "../common/color.js";
|
||||
import { CustomError } from "../common/error.js";
|
||||
import { kFormatter } from "../common/fmt.js";
|
||||
import { I18n } from "../common/I18n.js";
|
||||
import { icons, rankIcon } from "../common/icons.js";
|
||||
import { clampValue } from "../common/ops.js";
|
||||
import { flexLayout, measureText } from "../common/render.js";
|
||||
import { statCardLocales, wakatimeCardLocales } from "../translations.js";
|
||||
|
||||
const CARD_MIN_WIDTH = 287;
|
||||
const CARD_DEFAULT_WIDTH = 287;
|
||||
const RANK_CARD_MIN_WIDTH = 420;
|
||||
const RANK_CARD_DEFAULT_WIDTH = 450;
|
||||
const RANK_ONLY_CARD_MIN_WIDTH = 290;
|
||||
const RANK_ONLY_CARD_DEFAULT_WIDTH = 290;
|
||||
|
||||
/**
|
||||
* Long locales that need more space for text. Keep sorted alphabetically.
|
||||
*
|
||||
* @type {(keyof typeof wakatimeCardLocales["wakatimecard.title"])[]}
|
||||
*/
|
||||
const LONG_LOCALES = [
|
||||
"az",
|
||||
"bg",
|
||||
"cs",
|
||||
"de",
|
||||
"el",
|
||||
"es",
|
||||
"fil",
|
||||
"fi",
|
||||
"fr",
|
||||
"hu",
|
||||
"id",
|
||||
"ja",
|
||||
"ml",
|
||||
"my",
|
||||
"nl",
|
||||
"pl",
|
||||
"pt-br",
|
||||
"pt-pt",
|
||||
"ru",
|
||||
"sr",
|
||||
"sr-latn",
|
||||
"sw",
|
||||
"ta",
|
||||
"uk-ua",
|
||||
"uz",
|
||||
"zh-tw",
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a stats card text item.
|
||||
*
|
||||
* @param {object} params Object that contains the createTextNode parameters.
|
||||
* @param {string} params.icon The icon to display.
|
||||
* @param {string} params.label The label to display.
|
||||
* @param {number} params.value The value to display.
|
||||
* @param {string} params.id The id of the stat.
|
||||
* @param {string=} params.unitSymbol The unit symbol of the stat.
|
||||
* @param {number} params.index The index of the stat.
|
||||
* @param {boolean} params.showIcons Whether to show icons.
|
||||
* @param {number} params.shiftValuePos Number of pixels the value has to be shifted to the right.
|
||||
* @param {boolean} params.bold Whether to bold the label.
|
||||
* @param {string} params.numberFormat The format of numbers on card.
|
||||
* @param {number=} params.numberPrecision The precision of numbers on card.
|
||||
* @returns {string} The stats card text item SVG object.
|
||||
*/
|
||||
const createTextNode = ({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
id,
|
||||
unitSymbol,
|
||||
index,
|
||||
showIcons,
|
||||
shiftValuePos,
|
||||
bold,
|
||||
numberFormat,
|
||||
numberPrecision,
|
||||
}) => {
|
||||
const precision =
|
||||
typeof numberPrecision === "number" && !isNaN(numberPrecision)
|
||||
? clampValue(numberPrecision, 0, 2)
|
||||
: undefined;
|
||||
const kValue =
|
||||
numberFormat.toLowerCase() === "long" || id === "prs_merged_percentage"
|
||||
? value
|
||||
: kFormatter(value, precision);
|
||||
const staggerDelay = (index + 3) * 150;
|
||||
|
||||
const labelOffset = showIcons ? `x="25"` : "";
|
||||
const iconSvg = showIcons
|
||||
? `
|
||||
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||
${icon}
|
||||
</svg>
|
||||
`
|
||||
: "";
|
||||
return `
|
||||
<g class="stagger" style="animation-delay: ${staggerDelay}ms" transform="translate(25, 0)">
|
||||
${iconSvg}
|
||||
<text class="stat ${
|
||||
bold ? " bold" : "not_bold"
|
||||
}" ${labelOffset} y="12.5">${label}:</text>
|
||||
<text
|
||||
class="stat ${bold ? " bold" : "not_bold"}"
|
||||
x="${(showIcons ? 140 : 120) + shiftValuePos}"
|
||||
y="12.5"
|
||||
data-testid="${id}"
|
||||
>${kValue}${unitSymbol ? ` ${unitSymbol}` : ""}</text>
|
||||
</g>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates progress along the boundary of the circle, i.e. its circumference.
|
||||
*
|
||||
* @param {number} value The rank value to calculate progress for.
|
||||
* @returns {number} Progress value.
|
||||
*/
|
||||
const calculateCircleProgress = (value) => {
|
||||
const radius = 40;
|
||||
const c = Math.PI * (radius * 2);
|
||||
|
||||
if (value < 0) {
|
||||
value = 0;
|
||||
}
|
||||
if (value > 100) {
|
||||
value = 100;
|
||||
}
|
||||
|
||||
return ((100 - value) / 100) * c;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the animation to display progress along the circumference of circle
|
||||
* from the beginning to the given value in a clockwise direction.
|
||||
*
|
||||
* @param {{progress: number}} progress The progress value to animate to.
|
||||
* @returns {string} Progress animation css.
|
||||
*/
|
||||
const getProgressAnimation = ({ progress }) => {
|
||||
return `
|
||||
@keyframes rankAnimation {
|
||||
from {
|
||||
stroke-dashoffset: ${calculateCircleProgress(0)};
|
||||
}
|
||||
to {
|
||||
stroke-dashoffset: ${calculateCircleProgress(progress)};
|
||||
}
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves CSS styles for a card.
|
||||
*
|
||||
* @param {Object} colors The colors to use for the card.
|
||||
* @param {string} colors.titleColor The title color.
|
||||
* @param {string} colors.textColor The text color.
|
||||
* @param {string} colors.iconColor The icon color.
|
||||
* @param {string} colors.ringColor The ring color.
|
||||
* @param {boolean} colors.show_icons Whether to show icons.
|
||||
* @param {number} colors.progress The progress value to animate to.
|
||||
* @returns {string} Card CSS styles.
|
||||
*/
|
||||
const getStyles = ({
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
titleColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
ringColor,
|
||||
show_icons,
|
||||
progress,
|
||||
}) => {
|
||||
return `
|
||||
.stat {
|
||||
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
|
||||
}
|
||||
@supports(-moz-appearance: auto) {
|
||||
/* Selector detects Firefox */
|
||||
.stat { font-size:12px; }
|
||||
}
|
||||
.stagger {
|
||||
opacity: 0;
|
||||
animation: fadeInAnimation 0.3s ease-in-out forwards;
|
||||
}
|
||||
.rank-text {
|
||||
font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor};
|
||||
animation: scaleInAnimation 0.3s ease-in-out forwards;
|
||||
}
|
||||
.rank-percentile-header {
|
||||
font-size: 14px;
|
||||
}
|
||||
.rank-percentile-text {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.not_bold { font-weight: 400 }
|
||||
.bold { font-weight: 700 }
|
||||
.icon {
|
||||
fill: ${iconColor};
|
||||
display: ${show_icons ? "block" : "none"};
|
||||
}
|
||||
|
||||
.rank-circle-rim {
|
||||
stroke: ${ringColor};
|
||||
fill: none;
|
||||
stroke-width: 6;
|
||||
opacity: 0.2;
|
||||
}
|
||||
.rank-circle {
|
||||
stroke: ${ringColor};
|
||||
stroke-dasharray: 250;
|
||||
fill: none;
|
||||
stroke-width: 6;
|
||||
stroke-linecap: round;
|
||||
opacity: 0.8;
|
||||
transform-origin: -10px 8px;
|
||||
transform: rotate(-90deg);
|
||||
animation: rankAnimation 1s forwards ease-in-out;
|
||||
}
|
||||
${process.env.NODE_ENV === "test" ? "" : getProgressAnimation({ progress })}
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the label for commits according to the selected options
|
||||
*
|
||||
* @param {boolean} include_all_commits Option to include all years
|
||||
* @param {number|undefined} commits_year Option to include only selected year
|
||||
* @param {I18n} i18n The I18n instance.
|
||||
* @returns {string} The label corresponding to the options.
|
||||
*/
|
||||
const getTotalCommitsYearLabel = (include_all_commits, commits_year, i18n) =>
|
||||
include_all_commits
|
||||
? ""
|
||||
: commits_year
|
||||
? ` (${commits_year})`
|
||||
: ` (${i18n.t("wakatimecard.lastyear")})`;
|
||||
|
||||
/**
|
||||
* @typedef {import('../fetchers/types').StatsData} StatsData
|
||||
* @typedef {import('./types').StatCardOptions} StatCardOptions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Renders the stats card.
|
||||
*
|
||||
* @param {StatsData} stats The stats data.
|
||||
* @param {Partial<StatCardOptions>} options The card options.
|
||||
* @returns {string} The stats card SVG object.
|
||||
*/
|
||||
const renderStatsCard = (stats, options = {}) => {
|
||||
const {
|
||||
name,
|
||||
totalStars,
|
||||
totalCommits,
|
||||
totalIssues,
|
||||
totalPRs,
|
||||
totalPRsMerged,
|
||||
mergedPRsPercentage,
|
||||
totalReviews,
|
||||
totalDiscussionsStarted,
|
||||
totalDiscussionsAnswered,
|
||||
contributedTo,
|
||||
rank,
|
||||
} = stats;
|
||||
const {
|
||||
hide = [],
|
||||
show_icons = false,
|
||||
hide_title = false,
|
||||
hide_border = false,
|
||||
card_width,
|
||||
hide_rank = false,
|
||||
include_all_commits = false,
|
||||
commits_year,
|
||||
line_height = 25,
|
||||
title_color,
|
||||
ring_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
text_bold = true,
|
||||
bg_color,
|
||||
theme = "default",
|
||||
custom_title,
|
||||
border_radius,
|
||||
border_color,
|
||||
number_format = "short",
|
||||
number_precision,
|
||||
locale,
|
||||
disable_animations = false,
|
||||
rank_icon = "default",
|
||||
show = [],
|
||||
} = options;
|
||||
|
||||
const lheight = parseInt(String(line_height), 10);
|
||||
|
||||
// returns theme based colors with proper overrides and defaults
|
||||
const { titleColor, iconColor, textColor, bgColor, borderColor, ringColor } =
|
||||
getCardColors({
|
||||
title_color,
|
||||
text_color,
|
||||
icon_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
ring_color,
|
||||
theme,
|
||||
});
|
||||
|
||||
const apostrophe = /s$/i.test(name.trim()) ? "" : "s";
|
||||
const i18n = new I18n({
|
||||
locale,
|
||||
translations: {
|
||||
...statCardLocales({ name, apostrophe }),
|
||||
...wakatimeCardLocales,
|
||||
},
|
||||
});
|
||||
|
||||
// Meta data for creating text nodes with createTextNode function
|
||||
const STATS = {};
|
||||
|
||||
STATS.stars = {
|
||||
icon: icons.star,
|
||||
label: i18n.t("statcard.totalstars"),
|
||||
value: totalStars,
|
||||
id: "stars",
|
||||
};
|
||||
STATS.commits = {
|
||||
icon: icons.commits,
|
||||
label: `${i18n.t("statcard.commits")}${getTotalCommitsYearLabel(
|
||||
include_all_commits,
|
||||
commits_year,
|
||||
i18n,
|
||||
)}`,
|
||||
value: totalCommits,
|
||||
id: "commits",
|
||||
};
|
||||
STATS.prs = {
|
||||
icon: icons.prs,
|
||||
label: i18n.t("statcard.prs"),
|
||||
value: totalPRs,
|
||||
id: "prs",
|
||||
};
|
||||
|
||||
if (show.includes("prs_merged")) {
|
||||
STATS.prs_merged = {
|
||||
icon: icons.prs_merged,
|
||||
label: i18n.t("statcard.prs-merged"),
|
||||
value: totalPRsMerged,
|
||||
id: "prs_merged",
|
||||
};
|
||||
}
|
||||
|
||||
if (show.includes("prs_merged_percentage")) {
|
||||
STATS.prs_merged_percentage = {
|
||||
icon: icons.prs_merged_percentage,
|
||||
label: i18n.t("statcard.prs-merged-percentage"),
|
||||
value: mergedPRsPercentage.toFixed(
|
||||
typeof number_precision === "number" && !isNaN(number_precision)
|
||||
? clampValue(number_precision, 0, 2)
|
||||
: 2,
|
||||
),
|
||||
id: "prs_merged_percentage",
|
||||
unitSymbol: "%",
|
||||
};
|
||||
}
|
||||
|
||||
if (show.includes("reviews")) {
|
||||
STATS.reviews = {
|
||||
icon: icons.reviews,
|
||||
label: i18n.t("statcard.reviews"),
|
||||
value: totalReviews,
|
||||
id: "reviews",
|
||||
};
|
||||
}
|
||||
|
||||
STATS.issues = {
|
||||
icon: icons.issues,
|
||||
label: i18n.t("statcard.issues"),
|
||||
value: totalIssues,
|
||||
id: "issues",
|
||||
};
|
||||
|
||||
if (show.includes("discussions_started")) {
|
||||
STATS.discussions_started = {
|
||||
icon: icons.discussions_started,
|
||||
label: i18n.t("statcard.discussions-started"),
|
||||
value: totalDiscussionsStarted,
|
||||
id: "discussions_started",
|
||||
};
|
||||
}
|
||||
if (show.includes("discussions_answered")) {
|
||||
STATS.discussions_answered = {
|
||||
icon: icons.discussions_answered,
|
||||
label: i18n.t("statcard.discussions-answered"),
|
||||
value: totalDiscussionsAnswered,
|
||||
id: "discussions_answered",
|
||||
};
|
||||
}
|
||||
|
||||
STATS.contribs = {
|
||||
icon: icons.contribs,
|
||||
label: i18n.t("statcard.contribs"),
|
||||
value: contributedTo,
|
||||
id: "contribs",
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const isLongLocale = locale ? LONG_LOCALES.includes(locale) : false;
|
||||
|
||||
// filter out hidden stats defined by user & create the text nodes
|
||||
const statItems = Object.keys(STATS)
|
||||
.filter((key) => !hide.includes(key))
|
||||
.map((key, index) => {
|
||||
// @ts-ignore
|
||||
const stats = STATS[key];
|
||||
|
||||
// create the text nodes, and pass index so that we can calculate the line spacing
|
||||
return createTextNode({
|
||||
icon: stats.icon,
|
||||
label: stats.label,
|
||||
value: stats.value,
|
||||
id: stats.id,
|
||||
unitSymbol: stats.unitSymbol,
|
||||
index,
|
||||
showIcons: show_icons,
|
||||
shiftValuePos: 79.01 + (isLongLocale ? 50 : 0),
|
||||
bold: text_bold,
|
||||
numberFormat: number_format,
|
||||
numberPrecision: number_precision,
|
||||
});
|
||||
});
|
||||
|
||||
if (statItems.length === 0 && hide_rank) {
|
||||
throw new CustomError(
|
||||
"Could not render stats card.",
|
||||
"Either stats or rank are required.",
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate the card height depending on how many items there are
|
||||
// but if rank circle is visible clamp the minimum height to `150`
|
||||
let height = Math.max(
|
||||
45 + (statItems.length + 1) * lheight,
|
||||
hide_rank ? 0 : statItems.length ? 150 : 180,
|
||||
);
|
||||
|
||||
// the lower the user's percentile the better
|
||||
const progress = 100 - rank.percentile;
|
||||
const cssStyles = getStyles({
|
||||
titleColor,
|
||||
ringColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
show_icons,
|
||||
progress,
|
||||
});
|
||||
|
||||
const calculateTextWidth = () => {
|
||||
return measureText(
|
||||
custom_title
|
||||
? custom_title
|
||||
: statItems.length
|
||||
? i18n.t("statcard.title")
|
||||
: i18n.t("statcard.ranktitle"),
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
When hide_rank=true, the minimum card width is 270 px + the title length and padding.
|
||||
When hide_rank=false, the minimum card_width is 340 px + the icon width (if show_icons=true).
|
||||
Numbers are picked by looking at existing dimensions on production.
|
||||
*/
|
||||
const iconWidth = show_icons && statItems.length ? 16 + /* padding */ 1 : 0;
|
||||
const minCardWidth =
|
||||
(hide_rank
|
||||
? clampValue(
|
||||
50 /* padding */ + calculateTextWidth() * 2,
|
||||
CARD_MIN_WIDTH,
|
||||
Infinity,
|
||||
)
|
||||
: statItems.length
|
||||
? RANK_CARD_MIN_WIDTH
|
||||
: RANK_ONLY_CARD_MIN_WIDTH) + iconWidth;
|
||||
const defaultCardWidth =
|
||||
(hide_rank
|
||||
? CARD_DEFAULT_WIDTH
|
||||
: statItems.length
|
||||
? RANK_CARD_DEFAULT_WIDTH
|
||||
: RANK_ONLY_CARD_DEFAULT_WIDTH) + iconWidth;
|
||||
let width = card_width
|
||||
? isNaN(card_width)
|
||||
? defaultCardWidth
|
||||
: card_width
|
||||
: defaultCardWidth;
|
||||
if (width < minCardWidth) {
|
||||
width = minCardWidth;
|
||||
}
|
||||
|
||||
const card = new Card({
|
||||
customTitle: custom_title,
|
||||
defaultTitle: statItems.length
|
||||
? i18n.t("statcard.title")
|
||||
: i18n.t("statcard.ranktitle"),
|
||||
width,
|
||||
height,
|
||||
border_radius,
|
||||
colors: {
|
||||
titleColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
bgColor,
|
||||
borderColor,
|
||||
},
|
||||
});
|
||||
|
||||
card.setHideBorder(hide_border);
|
||||
card.setHideTitle(hide_title);
|
||||
card.setCSS(cssStyles);
|
||||
|
||||
if (disable_animations) {
|
||||
card.disableAnimations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the right rank circle translation values such that the rank circle
|
||||
* keeps respecting the following padding:
|
||||
*
|
||||
* width > RANK_CARD_DEFAULT_WIDTH: The default right padding of 70 px will be used.
|
||||
* width < RANK_CARD_DEFAULT_WIDTH: The left and right padding will be enlarged
|
||||
* equally from a certain minimum at RANK_CARD_MIN_WIDTH.
|
||||
*
|
||||
* @returns {number} - Rank circle translation value.
|
||||
*/
|
||||
const calculateRankXTranslation = () => {
|
||||
if (statItems.length) {
|
||||
const minXTranslation = RANK_CARD_MIN_WIDTH + iconWidth - 70;
|
||||
if (width > RANK_CARD_DEFAULT_WIDTH) {
|
||||
const xMaxExpansion = minXTranslation + (450 - minCardWidth) / 2;
|
||||
return xMaxExpansion + width - RANK_CARD_DEFAULT_WIDTH;
|
||||
} else {
|
||||
return minXTranslation + (width - minCardWidth) / 2;
|
||||
}
|
||||
} else {
|
||||
return width / 2 + 20 - 10;
|
||||
}
|
||||
};
|
||||
|
||||
// Conditionally rendered elements
|
||||
const rankCircle = hide_rank
|
||||
? ""
|
||||
: `<g data-testid="rank-circle"
|
||||
transform="translate(${calculateRankXTranslation()}, ${
|
||||
height / 2 - 50
|
||||
})">
|
||||
<circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
|
||||
<circle class="rank-circle" cx="-10" cy="8" r="40" />
|
||||
<g class="rank-text">
|
||||
${rankIcon(rank_icon, rank?.level, rank?.percentile)}
|
||||
</g>
|
||||
</g>`;
|
||||
|
||||
// Accessibility Labels
|
||||
const labels = Object.keys(STATS)
|
||||
.filter((key) => !hide.includes(key))
|
||||
.map((key) => {
|
||||
// @ts-ignore
|
||||
const stats = STATS[key];
|
||||
if (key === "commits") {
|
||||
return `${i18n.t("statcard.commits")} ${getTotalCommitsYearLabel(
|
||||
include_all_commits,
|
||||
commits_year,
|
||||
i18n,
|
||||
)} : ${stats.value}`;
|
||||
}
|
||||
return `${stats.label}: ${stats.value}`;
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
card.setAccessibilityLabel({
|
||||
title: `${card.title}, Rank: ${rank.level}`,
|
||||
desc: labels,
|
||||
});
|
||||
|
||||
return card.render(`
|
||||
${rankCircle}
|
||||
<svg x="0" y="0">
|
||||
${flexLayout({
|
||||
items: statItems,
|
||||
gap: lheight,
|
||||
direction: "column",
|
||||
}).join("")}
|
||||
</svg>
|
||||
`);
|
||||
};
|
||||
|
||||
export { renderStatsCard };
|
||||
export default renderStatsCard;
|
||||
@@ -0,0 +1,200 @@
|
||||
const { getCardColors, FlexLayout, clampValue } = require("../common/utils");
|
||||
const Card = require("../common/Card");
|
||||
|
||||
const createProgressNode = ({ width, color, name, progress }) => {
|
||||
const paddingRight = 95;
|
||||
const progressTextX = width - paddingRight + 10;
|
||||
const progressWidth = width - paddingRight;
|
||||
const progressPercentage = clampValue(progress, 2, 100);
|
||||
|
||||
return `
|
||||
<text data-testid="lang-name" x="2" y="15" class="lang-name">${name}</text>
|
||||
<text x="${progressTextX}" y="34" class="lang-name">${progress}%</text>
|
||||
<svg width="${progressWidth}">
|
||||
<rect rx="5" ry="5" x="0" y="25" width="${progressWidth}" height="8" fill="#ddd"></rect>
|
||||
<rect
|
||||
height="8"
|
||||
fill="${color}"
|
||||
rx="5" ry="5" x="0" y="25"
|
||||
data-testid="lang-progress"
|
||||
width="${progressPercentage}%"
|
||||
>
|
||||
</rect>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
|
||||
const createCompactLangNode = ({ lang, totalSize, x, y }) => {
|
||||
const percentage = ((lang.size / totalSize) * 100).toFixed(2);
|
||||
const color = lang.color || "#858585";
|
||||
|
||||
return `
|
||||
<g transform="translate(${x}, ${y})">
|
||||
<circle cx="5" cy="6" r="5" fill="${color}" />
|
||||
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
||||
${lang.name} ${percentage}%
|
||||
</text>
|
||||
</g>
|
||||
`;
|
||||
};
|
||||
|
||||
const createLanguageTextNode = ({ langs, totalSize, x, y }) => {
|
||||
return langs.map((lang, index) => {
|
||||
if (index % 2 === 0) {
|
||||
return createCompactLangNode({
|
||||
lang,
|
||||
x,
|
||||
y: 12.5 * index + y,
|
||||
totalSize,
|
||||
index,
|
||||
});
|
||||
}
|
||||
return createCompactLangNode({
|
||||
lang,
|
||||
x: 150,
|
||||
y: 12.5 + 12.5 * index,
|
||||
totalSize,
|
||||
index,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const lowercaseTrim = (name) => name.toLowerCase().trim();
|
||||
|
||||
const renderTopLanguages = (topLangs, options = {}) => {
|
||||
const {
|
||||
hide_title,
|
||||
hide_border,
|
||||
card_width,
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
hide,
|
||||
theme,
|
||||
layout,
|
||||
} = options;
|
||||
|
||||
let langs = Object.values(topLangs);
|
||||
let langsToHide = {};
|
||||
|
||||
// populate langsToHide map for quick lookup
|
||||
// while filtering out
|
||||
if (hide) {
|
||||
hide.forEach((langName) => {
|
||||
langsToHide[lowercaseTrim(langName)] = true;
|
||||
});
|
||||
}
|
||||
|
||||
// filter out langauges to be hidden
|
||||
langs = langs
|
||||
.sort((a, b) => b.size - a.size)
|
||||
.filter((lang) => {
|
||||
return !langsToHide[lowercaseTrim(lang.name)];
|
||||
});
|
||||
|
||||
const totalLanguageSize = langs.reduce((acc, curr) => {
|
||||
return acc + curr.size;
|
||||
}, 0);
|
||||
|
||||
// returns theme based colors with proper overrides and defaults
|
||||
const { titleColor, textColor, bgColor } = getCardColors({
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme,
|
||||
});
|
||||
|
||||
let width = isNaN(card_width) ? 300 : card_width;
|
||||
let height = 45 + (langs.length + 1) * 40;
|
||||
|
||||
let finalLayout = "";
|
||||
|
||||
// RENDER COMPACT LAYOUT
|
||||
if (layout === "compact") {
|
||||
width = width + 50;
|
||||
height = 30 + (langs.length / 2 + 1) * 40;
|
||||
|
||||
// progressOffset holds the previous language's width and used to offset the next language
|
||||
// so that we can stack them one after another, like this: [--][----][---]
|
||||
let progressOffset = 0;
|
||||
const compactProgressBar = langs
|
||||
.map((lang) => {
|
||||
const percentage = (
|
||||
(lang.size / totalLanguageSize) *
|
||||
(width - 50)
|
||||
).toFixed(2);
|
||||
|
||||
const progress =
|
||||
percentage < 10 ? parseFloat(percentage) + 10 : percentage;
|
||||
|
||||
const output = `
|
||||
<rect
|
||||
mask="url(#rect-mask)"
|
||||
data-testid="lang-progress"
|
||||
x="${progressOffset}"
|
||||
y="0"
|
||||
width="${progress}"
|
||||
height="8"
|
||||
fill="${lang.color || "#858585"}"
|
||||
/>
|
||||
`;
|
||||
progressOffset += parseFloat(percentage);
|
||||
return output;
|
||||
})
|
||||
.join("");
|
||||
|
||||
finalLayout = `
|
||||
<mask id="rect-mask">
|
||||
<rect x="0" y="0" width="${
|
||||
width - 50
|
||||
}" height="8" fill="white" rx="5" />
|
||||
</mask>
|
||||
${compactProgressBar}
|
||||
${createLanguageTextNode({
|
||||
x: 0,
|
||||
y: 25,
|
||||
langs,
|
||||
totalSize: totalLanguageSize,
|
||||
}).join("")}
|
||||
`;
|
||||
} else {
|
||||
finalLayout = FlexLayout({
|
||||
items: langs.map((lang) => {
|
||||
return createProgressNode({
|
||||
width: width,
|
||||
name: lang.name,
|
||||
color: lang.color || "#858585",
|
||||
progress: ((lang.size / totalLanguageSize) * 100).toFixed(2),
|
||||
});
|
||||
}),
|
||||
gap: 40,
|
||||
direction: "column",
|
||||
}).join("");
|
||||
}
|
||||
|
||||
const card = new Card({
|
||||
title: "Most Used Languages",
|
||||
width,
|
||||
height,
|
||||
colors: {
|
||||
titleColor,
|
||||
textColor,
|
||||
bgColor,
|
||||
},
|
||||
});
|
||||
|
||||
card.disableAnimations();
|
||||
card.setHideBorder(hide_border);
|
||||
card.setHideTitle(hide_title);
|
||||
card.setCSS(`
|
||||
.lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
|
||||
`);
|
||||
|
||||
return card.render(`
|
||||
<svg data-testid="lang-items" x="25">
|
||||
${finalLayout}
|
||||
</svg>
|
||||
`);
|
||||
};
|
||||
|
||||
module.exports = renderTopLanguages;
|
||||
@@ -1,965 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import { Card } from "../common/Card.js";
|
||||
import { getCardColors } from "../common/color.js";
|
||||
import { formatBytes } from "../common/fmt.js";
|
||||
import { I18n } from "../common/I18n.js";
|
||||
import { chunkArray, clampValue, lowercaseTrim } from "../common/ops.js";
|
||||
import {
|
||||
createProgressNode,
|
||||
flexLayout,
|
||||
measureText,
|
||||
} from "../common/render.js";
|
||||
import { langCardLocales } from "../translations.js";
|
||||
|
||||
const DEFAULT_CARD_WIDTH = 300;
|
||||
const MIN_CARD_WIDTH = 280;
|
||||
const DEFAULT_LANG_COLOR = "#858585";
|
||||
const CARD_PADDING = 25;
|
||||
const COMPACT_LAYOUT_BASE_HEIGHT = 90;
|
||||
const MAXIMUM_LANGS_COUNT = 20;
|
||||
|
||||
const NORMAL_LAYOUT_DEFAULT_LANGS_COUNT = 5;
|
||||
const COMPACT_LAYOUT_DEFAULT_LANGS_COUNT = 6;
|
||||
const DONUT_LAYOUT_DEFAULT_LANGS_COUNT = 5;
|
||||
const PIE_LAYOUT_DEFAULT_LANGS_COUNT = 6;
|
||||
const DONUT_VERTICAL_LAYOUT_DEFAULT_LANGS_COUNT = 6;
|
||||
|
||||
/**
|
||||
* @typedef {import("../fetchers/types").Lang} Lang
|
||||
*/
|
||||
|
||||
/**
|
||||
* Retrieves the programming language whose name is the longest.
|
||||
*
|
||||
* @param {Lang[]} arr Array of programming languages.
|
||||
* @returns {{ name: string, size: number, color: string }} Longest programming language object.
|
||||
*/
|
||||
const getLongestLang = (arr) =>
|
||||
arr.reduce(
|
||||
(savedLang, lang) =>
|
||||
lang.name.length > savedLang.name.length ? lang : savedLang,
|
||||
{ name: "", size: 0, color: "" },
|
||||
);
|
||||
|
||||
/**
|
||||
* Convert degrees to radians.
|
||||
*
|
||||
* @param {number} angleInDegrees Angle in degrees.
|
||||
* @returns {number} Angle in radians.
|
||||
*/
|
||||
const degreesToRadians = (angleInDegrees) => angleInDegrees * (Math.PI / 180.0);
|
||||
|
||||
/**
|
||||
* Convert radians to degrees.
|
||||
*
|
||||
* @param {number} angleInRadians Angle in radians.
|
||||
* @returns {number} Angle in degrees.
|
||||
*/
|
||||
const radiansToDegrees = (angleInRadians) => angleInRadians / (Math.PI / 180.0);
|
||||
|
||||
/**
|
||||
* Convert polar coordinates to cartesian coordinates.
|
||||
*
|
||||
* @param {number} centerX Center x coordinate.
|
||||
* @param {number} centerY Center y coordinate.
|
||||
* @param {number} radius Radius of the circle.
|
||||
* @param {number} angleInDegrees Angle in degrees.
|
||||
* @returns {{x: number, y: number}} Cartesian coordinates.
|
||||
*/
|
||||
const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => {
|
||||
const rads = degreesToRadians(angleInDegrees);
|
||||
return {
|
||||
x: centerX + radius * Math.cos(rads),
|
||||
y: centerY + radius * Math.sin(rads),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert cartesian coordinates to polar coordinates.
|
||||
*
|
||||
* @param {number} centerX Center x coordinate.
|
||||
* @param {number} centerY Center y coordinate.
|
||||
* @param {number} x Point x coordinate.
|
||||
* @param {number} y Point y coordinate.
|
||||
* @returns {{radius: number, angleInDegrees: number}} Polar coordinates.
|
||||
*/
|
||||
const cartesianToPolar = (centerX, centerY, x, y) => {
|
||||
const radius = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2));
|
||||
let angleInDegrees = radiansToDegrees(Math.atan2(y - centerY, x - centerX));
|
||||
if (angleInDegrees < 0) {
|
||||
angleInDegrees += 360;
|
||||
}
|
||||
return { radius, angleInDegrees };
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates length of circle.
|
||||
*
|
||||
* @param {number} radius Radius of the circle.
|
||||
* @returns {number} The length of the circle.
|
||||
*/
|
||||
const getCircleLength = (radius) => {
|
||||
return 2 * Math.PI * radius;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates height for the compact layout.
|
||||
*
|
||||
* @param {number} totalLangs Total number of languages.
|
||||
* @returns {number} Card height.
|
||||
*/
|
||||
const calculateCompactLayoutHeight = (totalLangs) => {
|
||||
return COMPACT_LAYOUT_BASE_HEIGHT + Math.round(totalLangs / 2) * 25;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates height for the normal layout.
|
||||
*
|
||||
* @param {number} totalLangs Total number of languages.
|
||||
* @returns {number} Card height.
|
||||
*/
|
||||
const calculateNormalLayoutHeight = (totalLangs) => {
|
||||
return 45 + (totalLangs + 1) * 40;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates height for the donut layout.
|
||||
*
|
||||
* @param {number} totalLangs Total number of languages.
|
||||
* @returns {number} Card height.
|
||||
*/
|
||||
const calculateDonutLayoutHeight = (totalLangs) => {
|
||||
return 215 + Math.max(totalLangs - 5, 0) * 32;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates height for the donut vertical layout.
|
||||
*
|
||||
* @param {number} totalLangs Total number of languages.
|
||||
* @returns {number} Card height.
|
||||
*/
|
||||
const calculateDonutVerticalLayoutHeight = (totalLangs) => {
|
||||
return 300 + Math.round(totalLangs / 2) * 25;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates height for the pie layout.
|
||||
*
|
||||
* @param {number} totalLangs Total number of languages.
|
||||
* @returns {number} Card height.
|
||||
*/
|
||||
const calculatePieLayoutHeight = (totalLangs) => {
|
||||
return 300 + Math.round(totalLangs / 2) * 25;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the center translation needed to keep the donut chart centred.
|
||||
* @param {number} totalLangs Total number of languages.
|
||||
* @returns {number} Donut center translation.
|
||||
*/
|
||||
const donutCenterTranslation = (totalLangs) => {
|
||||
return -45 + Math.max(totalLangs - 5, 0) * 16;
|
||||
};
|
||||
|
||||
/**
|
||||
* Trim top languages to lang_count while also hiding certain languages.
|
||||
*
|
||||
* @param {Record<string, Lang>} topLangs Top languages.
|
||||
* @param {number} langs_count Number of languages to show.
|
||||
* @param {string[]=} hide Languages to hide.
|
||||
* @returns {{ langs: Lang[], totalLanguageSize: number }} Trimmed top languages and total size.
|
||||
*/
|
||||
const trimTopLanguages = (topLangs, langs_count, hide) => {
|
||||
let langs = Object.values(topLangs);
|
||||
let langsToHide = {};
|
||||
let langsCount = clampValue(langs_count, 1, MAXIMUM_LANGS_COUNT);
|
||||
|
||||
// populate langsToHide map for quick lookup
|
||||
// while filtering out
|
||||
if (hide) {
|
||||
hide.forEach((langName) => {
|
||||
// @ts-ignore
|
||||
langsToHide[lowercaseTrim(langName)] = true;
|
||||
});
|
||||
}
|
||||
|
||||
// filter out languages to be hidden
|
||||
langs = langs
|
||||
.sort((a, b) => b.size - a.size)
|
||||
.filter((lang) => {
|
||||
// @ts-ignore
|
||||
return !langsToHide[lowercaseTrim(lang.name)];
|
||||
})
|
||||
.slice(0, langsCount);
|
||||
|
||||
const totalLanguageSize = langs.reduce((acc, curr) => acc + curr.size, 0);
|
||||
|
||||
return { langs, totalLanguageSize };
|
||||
};
|
||||
|
||||
/**
|
||||
* Get display value corresponding to the format.
|
||||
*
|
||||
* @param {number} size Bytes size.
|
||||
* @param {number} percentages Percentage value.
|
||||
* @param {string} format Format of the stats.
|
||||
* @returns {string} Display value.
|
||||
*/
|
||||
const getDisplayValue = (size, percentages, format) => {
|
||||
return format === "bytes" ? formatBytes(size) : `${percentages.toFixed(2)}%`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create progress bar text item for a programming language.
|
||||
*
|
||||
* @param {object} props Function properties.
|
||||
* @param {number} props.width The card width
|
||||
* @param {string} props.color Color of the programming language.
|
||||
* @param {string} props.name Name of the programming language.
|
||||
* @param {number} props.size Size of the programming language.
|
||||
* @param {number} props.totalSize Total size of all languages.
|
||||
* @param {string} props.statsFormat Stats format.
|
||||
* @param {number} props.index Index of the programming language.
|
||||
* @returns {string} Programming language SVG node.
|
||||
*/
|
||||
const createProgressTextNode = ({
|
||||
width,
|
||||
color,
|
||||
name,
|
||||
size,
|
||||
totalSize,
|
||||
statsFormat,
|
||||
index,
|
||||
}) => {
|
||||
const staggerDelay = (index + 3) * 150;
|
||||
const paddingRight = 95;
|
||||
const progressTextX = width - paddingRight + 10;
|
||||
const progressWidth = width - paddingRight;
|
||||
|
||||
const progress = (size / totalSize) * 100;
|
||||
const displayValue = getDisplayValue(size, progress, statsFormat);
|
||||
|
||||
return `
|
||||
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
|
||||
<text data-testid="lang-name" x="2" y="15" class="lang-name">${name}</text>
|
||||
<text x="${progressTextX}" y="34" class="lang-name">${displayValue}</text>
|
||||
${createProgressNode({
|
||||
x: 0,
|
||||
y: 25,
|
||||
color,
|
||||
width: progressWidth,
|
||||
progress,
|
||||
progressBarBackgroundColor: "#ddd",
|
||||
delay: staggerDelay + 300,
|
||||
})}
|
||||
</g>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates compact text item for a programming language.
|
||||
*
|
||||
* @param {object} props Function properties.
|
||||
* @param {Lang} props.lang Programming language object.
|
||||
* @param {number} props.totalSize Total size of all languages.
|
||||
* @param {boolean=} props.hideProgress Whether to hide percentage.
|
||||
* @param {string=} props.statsFormat Stats format
|
||||
* @param {number} props.index Index of the programming language.
|
||||
* @returns {string} Compact layout programming language SVG node.
|
||||
*/
|
||||
const createCompactLangNode = ({
|
||||
lang,
|
||||
totalSize,
|
||||
hideProgress,
|
||||
statsFormat = "percentages",
|
||||
index,
|
||||
}) => {
|
||||
const percentages = (lang.size / totalSize) * 100;
|
||||
const displayValue = getDisplayValue(lang.size, percentages, statsFormat);
|
||||
|
||||
const staggerDelay = (index + 3) * 150;
|
||||
const color = lang.color || "#858585";
|
||||
|
||||
return `
|
||||
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
|
||||
<circle cx="5" cy="6" r="5" fill="${color}" />
|
||||
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
||||
${lang.name} ${hideProgress ? "" : displayValue}
|
||||
</text>
|
||||
</g>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create compact languages text items for all programming languages.
|
||||
*
|
||||
* @param {object} props Function properties.
|
||||
* @param {Lang[]} props.langs Array of programming languages.
|
||||
* @param {number} props.totalSize Total size of all languages.
|
||||
* @param {boolean=} props.hideProgress Whether to hide percentage.
|
||||
* @param {string=} props.statsFormat Stats format
|
||||
* @returns {string} Programming languages SVG node.
|
||||
*/
|
||||
const createLanguageTextNode = ({
|
||||
langs,
|
||||
totalSize,
|
||||
hideProgress,
|
||||
statsFormat,
|
||||
}) => {
|
||||
const longestLang = getLongestLang(langs);
|
||||
const chunked = chunkArray(langs, langs.length / 2);
|
||||
const layouts = chunked.map((array) => {
|
||||
// @ts-ignore
|
||||
const items = array.map((lang, index) =>
|
||||
createCompactLangNode({
|
||||
lang,
|
||||
totalSize,
|
||||
hideProgress,
|
||||
statsFormat,
|
||||
index,
|
||||
}),
|
||||
);
|
||||
return flexLayout({
|
||||
items,
|
||||
gap: 25,
|
||||
direction: "column",
|
||||
}).join("");
|
||||
});
|
||||
|
||||
const percent = ((longestLang.size / totalSize) * 100).toFixed(2);
|
||||
const minGap = 150;
|
||||
const maxGap = 20 + measureText(`${longestLang.name} ${percent}%`, 11);
|
||||
return flexLayout({
|
||||
items: layouts,
|
||||
gap: maxGap < minGap ? minGap : maxGap,
|
||||
}).join("");
|
||||
};
|
||||
|
||||
/**
|
||||
* Create donut languages text items for all programming languages.
|
||||
*
|
||||
* @param {object} props Function properties.
|
||||
* @param {Lang[]} props.langs Array of programming languages.
|
||||
* @param {number} props.totalSize Total size of all languages.
|
||||
* @param {string} props.statsFormat Stats format
|
||||
* @returns {string} Donut layout programming language SVG node.
|
||||
*/
|
||||
const createDonutLanguagesNode = ({ langs, totalSize, statsFormat }) => {
|
||||
return flexLayout({
|
||||
items: langs.map((lang, index) => {
|
||||
return createCompactLangNode({
|
||||
lang,
|
||||
totalSize,
|
||||
hideProgress: false,
|
||||
statsFormat,
|
||||
index,
|
||||
});
|
||||
}),
|
||||
gap: 32,
|
||||
direction: "column",
|
||||
}).join("");
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the default language card layout.
|
||||
*
|
||||
* @param {Lang[]} langs Array of programming languages.
|
||||
* @param {number} width Card width.
|
||||
* @param {number} totalLanguageSize Total size of all languages.
|
||||
* @param {string} statsFormat Stats format.
|
||||
* @returns {string} Normal layout card SVG object.
|
||||
*/
|
||||
const renderNormalLayout = (langs, width, totalLanguageSize, statsFormat) => {
|
||||
return flexLayout({
|
||||
items: langs.map((lang, index) => {
|
||||
return createProgressTextNode({
|
||||
width,
|
||||
name: lang.name,
|
||||
color: lang.color || DEFAULT_LANG_COLOR,
|
||||
size: lang.size,
|
||||
totalSize: totalLanguageSize,
|
||||
statsFormat,
|
||||
index,
|
||||
});
|
||||
}),
|
||||
gap: 40,
|
||||
direction: "column",
|
||||
}).join("");
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the compact language card layout.
|
||||
*
|
||||
* @param {Lang[]} langs Array of programming languages.
|
||||
* @param {number} width Card width.
|
||||
* @param {number} totalLanguageSize Total size of all languages.
|
||||
* @param {boolean=} hideProgress Whether to hide progress bar.
|
||||
* @param {string} statsFormat Stats format.
|
||||
* @returns {string} Compact layout card SVG object.
|
||||
*/
|
||||
const renderCompactLayout = (
|
||||
langs,
|
||||
width,
|
||||
totalLanguageSize,
|
||||
hideProgress,
|
||||
statsFormat = "percentages",
|
||||
) => {
|
||||
const paddingRight = 50;
|
||||
const offsetWidth = width - paddingRight;
|
||||
// progressOffset holds the previous language's width and used to offset the next language
|
||||
// so that we can stack them one after another, like this: [--][----][---]
|
||||
let progressOffset = 0;
|
||||
const compactProgressBar = langs
|
||||
.map((lang) => {
|
||||
const percentage = parseFloat(
|
||||
((lang.size / totalLanguageSize) * offsetWidth).toFixed(2),
|
||||
);
|
||||
|
||||
const progress = percentage < 10 ? percentage + 10 : percentage;
|
||||
|
||||
const output = `
|
||||
<rect
|
||||
mask="url(#rect-mask)"
|
||||
data-testid="lang-progress"
|
||||
x="${progressOffset}"
|
||||
y="0"
|
||||
width="${progress}"
|
||||
height="8"
|
||||
fill="${lang.color || "#858585"}"
|
||||
/>
|
||||
`;
|
||||
progressOffset += percentage;
|
||||
return output;
|
||||
})
|
||||
.join("");
|
||||
|
||||
return `
|
||||
${
|
||||
hideProgress
|
||||
? ""
|
||||
: `
|
||||
<mask id="rect-mask">
|
||||
<rect x="0" y="0" width="${offsetWidth}" height="8" fill="white" rx="5"/>
|
||||
</mask>
|
||||
${compactProgressBar}
|
||||
`
|
||||
}
|
||||
<g transform="translate(0, ${hideProgress ? "0" : "25"})">
|
||||
${createLanguageTextNode({
|
||||
langs,
|
||||
totalSize: totalLanguageSize,
|
||||
hideProgress,
|
||||
statsFormat,
|
||||
})}
|
||||
</g>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders donut vertical layout to display user's most frequently used programming languages.
|
||||
*
|
||||
* @param {Lang[]} langs Array of programming languages.
|
||||
* @param {number} totalLanguageSize Total size of all languages.
|
||||
* @param {string} statsFormat Stats format.
|
||||
* @returns {string} Compact layout card SVG object.
|
||||
*/
|
||||
const renderDonutVerticalLayout = (langs, totalLanguageSize, statsFormat) => {
|
||||
// Donut vertical chart radius and total length
|
||||
const radius = 80;
|
||||
const totalCircleLength = getCircleLength(radius);
|
||||
|
||||
// SVG circles
|
||||
let circles = [];
|
||||
|
||||
// Start indent for donut vertical chart parts
|
||||
let indent = 0;
|
||||
|
||||
// Start delay coefficient for donut vertical chart parts
|
||||
let startDelayCoefficient = 1;
|
||||
|
||||
// Generate each donut vertical chart part
|
||||
for (const lang of langs) {
|
||||
const percentage = (lang.size / totalLanguageSize) * 100;
|
||||
const circleLength = totalCircleLength * (percentage / 100);
|
||||
const delay = startDelayCoefficient * 100;
|
||||
|
||||
circles.push(`
|
||||
<g class="stagger" style="animation-delay: ${delay}ms">
|
||||
<circle
|
||||
cx="150"
|
||||
cy="100"
|
||||
r="${radius}"
|
||||
fill="transparent"
|
||||
stroke="${lang.color}"
|
||||
stroke-width="25"
|
||||
stroke-dasharray="${totalCircleLength}"
|
||||
stroke-dashoffset="${indent}"
|
||||
size="${percentage}"
|
||||
data-testid="lang-donut"
|
||||
/>
|
||||
</g>
|
||||
`);
|
||||
|
||||
// Update the indent for the next part
|
||||
indent += circleLength;
|
||||
// Update the start delay coefficient for the next part
|
||||
startDelayCoefficient += 1;
|
||||
}
|
||||
|
||||
return `
|
||||
<svg data-testid="lang-items">
|
||||
<g transform="translate(0, 0)">
|
||||
<svg data-testid="donut">
|
||||
${circles.join("")}
|
||||
</svg>
|
||||
</g>
|
||||
<g transform="translate(0, 220)">
|
||||
<svg data-testid="lang-names" x="${CARD_PADDING}">
|
||||
${createLanguageTextNode({
|
||||
langs,
|
||||
totalSize: totalLanguageSize,
|
||||
hideProgress: false,
|
||||
statsFormat,
|
||||
})}
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders pie layout to display user's most frequently used programming languages.
|
||||
*
|
||||
* @param {Lang[]} langs Array of programming languages.
|
||||
* @param {number} totalLanguageSize Total size of all languages.
|
||||
* @param {string} statsFormat Stats format.
|
||||
* @returns {string} Compact layout card SVG object.
|
||||
*/
|
||||
const renderPieLayout = (langs, totalLanguageSize, statsFormat) => {
|
||||
// Pie chart radius and center coordinates
|
||||
const radius = 90;
|
||||
const centerX = 150;
|
||||
const centerY = 100;
|
||||
|
||||
// Start angle for the pie chart parts
|
||||
let startAngle = 0;
|
||||
|
||||
// Start delay coefficient for the pie chart parts
|
||||
let startDelayCoefficient = 1;
|
||||
|
||||
// SVG paths
|
||||
const paths = [];
|
||||
|
||||
// Generate each pie chart part
|
||||
for (const lang of langs) {
|
||||
if (langs.length === 1) {
|
||||
paths.push(`
|
||||
<circle
|
||||
cx="${centerX}"
|
||||
cy="${centerY}"
|
||||
r="${radius}"
|
||||
stroke="none"
|
||||
fill="${lang.color}"
|
||||
data-testid="lang-pie"
|
||||
size="100"
|
||||
/>
|
||||
`);
|
||||
break;
|
||||
}
|
||||
|
||||
const langSizePart = lang.size / totalLanguageSize;
|
||||
const percentage = langSizePart * 100;
|
||||
// Calculate the angle for the current part
|
||||
const angle = langSizePart * 360;
|
||||
|
||||
// Calculate the end angle
|
||||
const endAngle = startAngle + angle;
|
||||
|
||||
// Calculate the coordinates of the start and end points of the arc
|
||||
const startPoint = polarToCartesian(centerX, centerY, radius, startAngle);
|
||||
const endPoint = polarToCartesian(centerX, centerY, radius, endAngle);
|
||||
|
||||
// Determine the large arc flag based on the angle
|
||||
const largeArcFlag = angle > 180 ? 1 : 0;
|
||||
|
||||
// Calculate delay
|
||||
const delay = startDelayCoefficient * 100;
|
||||
|
||||
// SVG arc markup
|
||||
paths.push(`
|
||||
<g class="stagger" style="animation-delay: ${delay}ms">
|
||||
<path
|
||||
data-testid="lang-pie"
|
||||
size="${percentage}"
|
||||
d="M ${centerX} ${centerY} L ${startPoint.x} ${startPoint.y} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endPoint.x} ${endPoint.y} Z"
|
||||
fill="${lang.color}"
|
||||
/>
|
||||
</g>
|
||||
`);
|
||||
|
||||
// Update the start angle for the next part
|
||||
startAngle = endAngle;
|
||||
// Update the start delay coefficient for the next part
|
||||
startDelayCoefficient += 1;
|
||||
}
|
||||
|
||||
return `
|
||||
<svg data-testid="lang-items">
|
||||
<g transform="translate(0, 0)">
|
||||
<svg data-testid="pie">
|
||||
${paths.join("")}
|
||||
</svg>
|
||||
</g>
|
||||
<g transform="translate(0, 220)">
|
||||
<svg data-testid="lang-names" x="${CARD_PADDING}">
|
||||
${createLanguageTextNode({
|
||||
langs,
|
||||
totalSize: totalLanguageSize,
|
||||
hideProgress: false,
|
||||
statsFormat,
|
||||
})}
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the SVG paths for the language donut chart.
|
||||
*
|
||||
* @param {number} cx Donut center x-position.
|
||||
* @param {number} cy Donut center y-position.
|
||||
* @param {number} radius Donut arc Radius.
|
||||
* @param {number[]} percentages Array with donut section percentages.
|
||||
* @returns {{d: string, percent: number}[]} Array of svg path elements
|
||||
*/
|
||||
const createDonutPaths = (cx, cy, radius, percentages) => {
|
||||
const paths = [];
|
||||
let startAngle = 0;
|
||||
let endAngle = 0;
|
||||
|
||||
const totalPercent = percentages.reduce((acc, curr) => acc + curr, 0);
|
||||
for (let i = 0; i < percentages.length; i++) {
|
||||
const tmpPath = {};
|
||||
|
||||
let percent = parseFloat(
|
||||
((percentages[i] / totalPercent) * 100).toFixed(2),
|
||||
);
|
||||
|
||||
endAngle = 3.6 * percent + startAngle;
|
||||
const startPoint = polarToCartesian(cx, cy, radius, endAngle - 90); // rotate donut 90 degrees counter-clockwise.
|
||||
const endPoint = polarToCartesian(cx, cy, radius, startAngle - 90); // rotate donut 90 degrees counter-clockwise.
|
||||
const largeArc = endAngle - startAngle <= 180 ? 0 : 1;
|
||||
|
||||
tmpPath.percent = percent;
|
||||
tmpPath.d = `M ${startPoint.x} ${startPoint.y} A ${radius} ${radius} 0 ${largeArc} 0 ${endPoint.x} ${endPoint.y}`;
|
||||
|
||||
paths.push(tmpPath);
|
||||
startAngle = endAngle;
|
||||
}
|
||||
|
||||
return paths;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the donut language card layout.
|
||||
*
|
||||
* @param {Lang[]} langs Array of programming languages.
|
||||
* @param {number} width Card width.
|
||||
* @param {number} totalLanguageSize Total size of all languages.
|
||||
* @param {string} statsFormat Stats format.
|
||||
* @returns {string} Donut layout card SVG object.
|
||||
*/
|
||||
const renderDonutLayout = (langs, width, totalLanguageSize, statsFormat) => {
|
||||
const centerX = width / 3;
|
||||
const centerY = width / 3;
|
||||
const radius = centerX - 60;
|
||||
const strokeWidth = 12;
|
||||
|
||||
const colors = langs.map((lang) => lang.color);
|
||||
const langsPercents = langs.map((lang) =>
|
||||
parseFloat(((lang.size / totalLanguageSize) * 100).toFixed(2)),
|
||||
);
|
||||
|
||||
const langPaths = createDonutPaths(centerX, centerY, radius, langsPercents);
|
||||
|
||||
const donutPaths =
|
||||
langs.length === 1
|
||||
? `<circle cx="${centerX}" cy="${centerY}" r="${radius}" stroke="${colors[0]}" fill="none" stroke-width="${strokeWidth}" data-testid="lang-donut" size="100"/>`
|
||||
: langPaths
|
||||
.map((section, index) => {
|
||||
const staggerDelay = (index + 3) * 100;
|
||||
const delay = staggerDelay + 300;
|
||||
|
||||
const output = `
|
||||
<g class="stagger" style="animation-delay: ${delay}ms">
|
||||
<path
|
||||
data-testid="lang-donut"
|
||||
size="${section.percent}"
|
||||
d="${section.d}"
|
||||
stroke="${colors[index]}"
|
||||
fill="none"
|
||||
stroke-width="${strokeWidth}">
|
||||
</path>
|
||||
</g>
|
||||
`;
|
||||
|
||||
return output;
|
||||
})
|
||||
.join("");
|
||||
|
||||
const donut = `<svg width="${width}" height="${width}">${donutPaths}</svg>`;
|
||||
|
||||
return `
|
||||
<g transform="translate(0, 0)">
|
||||
<g transform="translate(0, 0)">
|
||||
${createDonutLanguagesNode({ langs, totalSize: totalLanguageSize, statsFormat })}
|
||||
</g>
|
||||
|
||||
<g transform="translate(125, ${donutCenterTranslation(langs.length)})">
|
||||
${donut}
|
||||
</g>
|
||||
</g>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {import("./types").TopLangOptions} TopLangOptions
|
||||
* @typedef {TopLangOptions["layout"]} Layout
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates the no languages data SVG node.
|
||||
*
|
||||
* @param {object} props Object with function properties.
|
||||
* @param {string} props.color No languages data text color.
|
||||
* @param {string} props.text No languages data translated text.
|
||||
* @param {Layout | undefined} props.layout Card layout.
|
||||
* @returns {string} No languages data SVG node string.
|
||||
*/
|
||||
const noLanguagesDataNode = ({ color, text, layout }) => {
|
||||
return `
|
||||
<text x="${
|
||||
layout === "pie" || layout === "donut-vertical" ? CARD_PADDING : 0
|
||||
}" y="11" class="stat bold" fill="${color}">${text}</text>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get default languages count for provided card layout.
|
||||
*
|
||||
* @param {object} props Function properties.
|
||||
* @param {Layout=} props.layout Input layout string.
|
||||
* @param {boolean=} props.hide_progress Input hide_progress parameter value.
|
||||
* @returns {number} Default languages count for input layout.
|
||||
*/
|
||||
const getDefaultLanguagesCountByLayout = ({ layout, hide_progress }) => {
|
||||
if (layout === "compact" || hide_progress === true) {
|
||||
return COMPACT_LAYOUT_DEFAULT_LANGS_COUNT;
|
||||
} else if (layout === "donut") {
|
||||
return DONUT_LAYOUT_DEFAULT_LANGS_COUNT;
|
||||
} else if (layout === "donut-vertical") {
|
||||
return DONUT_VERTICAL_LAYOUT_DEFAULT_LANGS_COUNT;
|
||||
} else if (layout === "pie") {
|
||||
return PIE_LAYOUT_DEFAULT_LANGS_COUNT;
|
||||
} else {
|
||||
return NORMAL_LAYOUT_DEFAULT_LANGS_COUNT;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {import('../fetchers/types').TopLangData} TopLangData
|
||||
*/
|
||||
|
||||
/**
|
||||
* Renders card that display user's most frequently used programming languages.
|
||||
*
|
||||
* @param {TopLangData} topLangs User's most frequently used programming languages.
|
||||
* @param {Partial<TopLangOptions>} options Card options.
|
||||
* @returns {string} Language card SVG object.
|
||||
*/
|
||||
const renderTopLanguages = (topLangs, options = {}) => {
|
||||
const {
|
||||
hide_title = false,
|
||||
hide_border = false,
|
||||
card_width,
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
hide,
|
||||
hide_progress,
|
||||
theme,
|
||||
layout,
|
||||
custom_title,
|
||||
locale,
|
||||
langs_count = getDefaultLanguagesCountByLayout({ layout, hide_progress }),
|
||||
border_radius,
|
||||
border_color,
|
||||
disable_animations,
|
||||
stats_format = "percentages",
|
||||
} = options;
|
||||
|
||||
const i18n = new I18n({
|
||||
locale,
|
||||
translations: langCardLocales,
|
||||
});
|
||||
|
||||
const { langs, totalLanguageSize } = trimTopLanguages(
|
||||
topLangs,
|
||||
langs_count,
|
||||
hide,
|
||||
);
|
||||
|
||||
let width = card_width
|
||||
? isNaN(card_width)
|
||||
? DEFAULT_CARD_WIDTH
|
||||
: card_width < MIN_CARD_WIDTH
|
||||
? MIN_CARD_WIDTH
|
||||
: card_width
|
||||
: DEFAULT_CARD_WIDTH;
|
||||
let height = calculateNormalLayoutHeight(langs.length);
|
||||
|
||||
// returns theme based colors with proper overrides and defaults
|
||||
const colors = getCardColors({
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
});
|
||||
|
||||
let finalLayout = "";
|
||||
if (langs.length === 0) {
|
||||
height = COMPACT_LAYOUT_BASE_HEIGHT;
|
||||
finalLayout = noLanguagesDataNode({
|
||||
color: colors.textColor,
|
||||
text: i18n.t("langcard.nodata"),
|
||||
layout,
|
||||
});
|
||||
} else if (layout === "pie") {
|
||||
height = calculatePieLayoutHeight(langs.length);
|
||||
finalLayout = renderPieLayout(langs, totalLanguageSize, stats_format);
|
||||
} else if (layout === "donut-vertical") {
|
||||
height = calculateDonutVerticalLayoutHeight(langs.length);
|
||||
finalLayout = renderDonutVerticalLayout(
|
||||
langs,
|
||||
totalLanguageSize,
|
||||
stats_format,
|
||||
);
|
||||
} else if (layout === "compact" || hide_progress == true) {
|
||||
height =
|
||||
calculateCompactLayoutHeight(langs.length) + (hide_progress ? -25 : 0);
|
||||
|
||||
finalLayout = renderCompactLayout(
|
||||
langs,
|
||||
width,
|
||||
totalLanguageSize,
|
||||
hide_progress,
|
||||
stats_format,
|
||||
);
|
||||
} else if (layout === "donut") {
|
||||
height = calculateDonutLayoutHeight(langs.length);
|
||||
width = width + 50; // padding
|
||||
finalLayout = renderDonutLayout(
|
||||
langs,
|
||||
width,
|
||||
totalLanguageSize,
|
||||
stats_format,
|
||||
);
|
||||
} else {
|
||||
finalLayout = renderNormalLayout(
|
||||
langs,
|
||||
width,
|
||||
totalLanguageSize,
|
||||
stats_format,
|
||||
);
|
||||
}
|
||||
|
||||
const card = new Card({
|
||||
customTitle: custom_title,
|
||||
defaultTitle: i18n.t("langcard.title"),
|
||||
width,
|
||||
height,
|
||||
border_radius,
|
||||
colors,
|
||||
});
|
||||
|
||||
if (disable_animations) {
|
||||
card.disableAnimations();
|
||||
}
|
||||
|
||||
card.setHideBorder(hide_border);
|
||||
card.setHideTitle(hide_title);
|
||||
card.setCSS(
|
||||
`
|
||||
@keyframes slideInAnimation {
|
||||
from {
|
||||
width: 0;
|
||||
}
|
||||
to {
|
||||
width: calc(100%-100px);
|
||||
}
|
||||
}
|
||||
@keyframes growWidthAnimation {
|
||||
from {
|
||||
width: 0;
|
||||
}
|
||||
to {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.stat {
|
||||
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${colors.textColor};
|
||||
}
|
||||
@supports(-moz-appearance: auto) {
|
||||
/* Selector detects Firefox */
|
||||
.stat { font-size:12px; }
|
||||
}
|
||||
.bold { font-weight: 700 }
|
||||
.lang-name {
|
||||
font: 400 11px "Segoe UI", Ubuntu, Sans-Serif;
|
||||
fill: ${colors.textColor};
|
||||
}
|
||||
.stagger {
|
||||
opacity: 0;
|
||||
animation: fadeInAnimation 0.3s ease-in-out forwards;
|
||||
}
|
||||
#rect-mask rect{
|
||||
animation: slideInAnimation 1s ease-in-out forwards;
|
||||
}
|
||||
.lang-progress{
|
||||
animation: growWidthAnimation 0.6s ease-in-out forwards;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
if (layout === "pie" || layout === "donut-vertical") {
|
||||
return card.render(finalLayout);
|
||||
}
|
||||
|
||||
return card.render(`
|
||||
<svg data-testid="lang-items" x="${CARD_PADDING}">
|
||||
${finalLayout}
|
||||
</svg>
|
||||
`);
|
||||
};
|
||||
|
||||
export {
|
||||
getLongestLang,
|
||||
degreesToRadians,
|
||||
radiansToDegrees,
|
||||
polarToCartesian,
|
||||
cartesianToPolar,
|
||||
getCircleLength,
|
||||
calculateCompactLayoutHeight,
|
||||
calculateNormalLayoutHeight,
|
||||
calculateDonutLayoutHeight,
|
||||
calculateDonutVerticalLayoutHeight,
|
||||
calculatePieLayoutHeight,
|
||||
donutCenterTranslation,
|
||||
trimTopLanguages,
|
||||
renderTopLanguages,
|
||||
MIN_CARD_WIDTH,
|
||||
getDefaultLanguagesCountByLayout,
|
||||
};
|
||||
Vendored
-67
@@ -1,67 +0,0 @@
|
||||
type ThemeNames = keyof typeof import("../../themes/index.js");
|
||||
type RankIcon = "default" | "github" | "percentile";
|
||||
|
||||
export type CommonOptions = {
|
||||
title_color: string;
|
||||
icon_color: string;
|
||||
text_color: string;
|
||||
bg_color: string;
|
||||
theme: ThemeNames;
|
||||
border_radius: number;
|
||||
border_color: string;
|
||||
locale: string;
|
||||
hide_border: boolean;
|
||||
};
|
||||
|
||||
export type StatCardOptions = CommonOptions & {
|
||||
hide: string[];
|
||||
show_icons: boolean;
|
||||
hide_title: boolean;
|
||||
card_width: number;
|
||||
hide_rank: boolean;
|
||||
include_all_commits: boolean;
|
||||
commits_year: number;
|
||||
line_height: number | string;
|
||||
custom_title: string;
|
||||
disable_animations: boolean;
|
||||
number_format: string;
|
||||
number_precision: number;
|
||||
ring_color: string;
|
||||
text_bold: boolean;
|
||||
rank_icon: RankIcon;
|
||||
show: string[];
|
||||
};
|
||||
|
||||
export type RepoCardOptions = CommonOptions & {
|
||||
show_owner: boolean;
|
||||
description_lines_count: number;
|
||||
};
|
||||
|
||||
export type TopLangOptions = CommonOptions & {
|
||||
hide_title: boolean;
|
||||
card_width: number;
|
||||
hide: string[];
|
||||
layout: "compact" | "normal" | "donut" | "donut-vertical" | "pie";
|
||||
custom_title: string;
|
||||
langs_count: number;
|
||||
disable_animations: boolean;
|
||||
hide_progress: boolean;
|
||||
stats_format: "percentages" | "bytes";
|
||||
};
|
||||
|
||||
export type WakaTimeOptions = CommonOptions & {
|
||||
hide_title: boolean;
|
||||
hide: string[];
|
||||
card_width: number;
|
||||
line_height: string;
|
||||
hide_progress: boolean;
|
||||
custom_title: string;
|
||||
layout: "compact" | "normal";
|
||||
langs_count: number;
|
||||
display_format: "time" | "percent";
|
||||
disable_animations: boolean;
|
||||
};
|
||||
|
||||
export type GistCardOptions = CommonOptions & {
|
||||
show_owner: boolean;
|
||||
};
|
||||
@@ -1,481 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import { Card } from "../common/Card.js";
|
||||
import { getCardColors } from "../common/color.js";
|
||||
import { I18n } from "../common/I18n.js";
|
||||
import { clampValue, lowercaseTrim } from "../common/ops.js";
|
||||
import { createProgressNode, flexLayout } from "../common/render.js";
|
||||
import { wakatimeCardLocales } from "../translations.js";
|
||||
|
||||
/** Import language colors.
|
||||
*
|
||||
* @description Here we use the workaround found in
|
||||
* https://stackoverflow.com/questions/66726365/how-should-i-import-json-in-node
|
||||
* since vercel is using v16.14.0 which does not yet support json imports without the
|
||||
* --experimental-json-modules flag.
|
||||
*/
|
||||
import { createRequire } from "module";
|
||||
const require = createRequire(import.meta.url);
|
||||
const languageColors = require("../common/languageColors.json"); // now works
|
||||
|
||||
const DEFAULT_CARD_WIDTH = 495;
|
||||
const MIN_CARD_WIDTH = 250;
|
||||
const COMPACT_LAYOUT_MIN_WIDTH = 400;
|
||||
const DEFAULT_LINE_HEIGHT = 25;
|
||||
const PROGRESSBAR_PADDING = 130;
|
||||
const HIDDEN_PROGRESSBAR_PADDING = 170;
|
||||
const COMPACT_LAYOUT_PROGRESSBAR_PADDING = 25;
|
||||
const TOTAL_TEXT_WIDTH = 275;
|
||||
|
||||
/**
|
||||
* Creates the no coding activity SVG node.
|
||||
*
|
||||
* @param {object} props The function properties.
|
||||
* @param {string} props.color No coding activity text color.
|
||||
* @param {string} props.text No coding activity translated text.
|
||||
* @returns {string} No coding activity SVG node string.
|
||||
*/
|
||||
const noCodingActivityNode = ({ color, text }) => {
|
||||
return `
|
||||
<text x="25" y="11" class="stat bold" fill="${color}">${text}</text>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {import('../fetchers/types').WakaTimeLang} WakaTimeLang
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format language value.
|
||||
*
|
||||
* @param {Object} args The function arguments.
|
||||
* @param {WakaTimeLang} args.lang The language object.
|
||||
* @param {"time" | "percent"} args.display_format The display format of the language node.
|
||||
* @returns {string} The formatted language value.
|
||||
*/
|
||||
const formatLanguageValue = ({ display_format, lang }) => {
|
||||
return display_format === "percent"
|
||||
? `${lang.percent.toFixed(2).toString()} %`
|
||||
: lang.text;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create compact WakaTime layout.
|
||||
*
|
||||
* @param {Object} args The function arguments.
|
||||
* @param {WakaTimeLang} args.lang The languages array.
|
||||
* @param {number} args.x The x position of the language node.
|
||||
* @param {number} args.y The y position of the language node.
|
||||
* @param {"time" | "percent"} args.display_format The display format of the language node.
|
||||
* @returns {string} The compact layout language SVG node.
|
||||
*/
|
||||
const createCompactLangNode = ({ lang, x, y, display_format }) => {
|
||||
// @ts-ignore
|
||||
const color = languageColors[lang.name] || "#858585";
|
||||
const value = formatLanguageValue({ display_format, lang });
|
||||
|
||||
return `
|
||||
<g transform="translate(${x}, ${y})">
|
||||
<circle cx="5" cy="6" r="5" fill="${color}" />
|
||||
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
||||
${lang.name} - ${value}
|
||||
</text>
|
||||
</g>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create WakaTime language text node item.
|
||||
*
|
||||
* @param {Object} args The function arguments.
|
||||
* @param {WakaTimeLang[]} args.langs The language objects.
|
||||
* @param {number} args.y The y position of the language node.
|
||||
* @param {"time" | "percent"} args.display_format The display format of the language node.
|
||||
* @param {number} args.card_width Width in px of the card.
|
||||
* @returns {string[]} The language text node items.
|
||||
*/
|
||||
const createLanguageTextNode = ({ langs, y, display_format, card_width }) => {
|
||||
const LEFT_X = 25;
|
||||
const RIGHT_X_BASE = 230;
|
||||
const rightOffset = (card_width - DEFAULT_CARD_WIDTH) / 2;
|
||||
const RIGHT_X = RIGHT_X_BASE + rightOffset;
|
||||
|
||||
return langs.map((lang, index) => {
|
||||
const isLeft = index % 2 === 0;
|
||||
return createCompactLangNode({
|
||||
lang,
|
||||
x: isLeft ? LEFT_X : RIGHT_X,
|
||||
y: y + DEFAULT_LINE_HEIGHT * Math.floor(index / 2),
|
||||
display_format,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create WakaTime text item.
|
||||
*
|
||||
* @param {Object} args The function arguments.
|
||||
* @param {string} args.id The id of the text node item.
|
||||
* @param {string} args.label The label of the text node item.
|
||||
* @param {string} args.value The value of the text node item.
|
||||
* @param {number} args.index The index of the text node item.
|
||||
* @param {number} args.percent Percentage of the text node item.
|
||||
* @param {boolean=} args.hideProgress Whether to hide the progress bar.
|
||||
* @param {string} args.progressBarColor The color of the progress bar.
|
||||
* @param {string} args.progressBarBackgroundColor The color of the progress bar background.
|
||||
* @param {number} args.progressBarWidth The width of the progress bar.
|
||||
* @returns {string} The text SVG node.
|
||||
*/
|
||||
const createTextNode = ({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
index,
|
||||
percent,
|
||||
hideProgress,
|
||||
progressBarColor,
|
||||
progressBarBackgroundColor,
|
||||
progressBarWidth,
|
||||
}) => {
|
||||
const staggerDelay = (index + 3) * 150;
|
||||
const cardProgress = hideProgress
|
||||
? null
|
||||
: createProgressNode({
|
||||
x: 110,
|
||||
y: 4,
|
||||
progress: percent,
|
||||
color: progressBarColor,
|
||||
width: progressBarWidth,
|
||||
// @ts-ignore
|
||||
name: label,
|
||||
progressBarBackgroundColor,
|
||||
delay: staggerDelay + 300,
|
||||
});
|
||||
|
||||
return `
|
||||
<g class="stagger" style="animation-delay: ${staggerDelay}ms" transform="translate(25, 0)">
|
||||
<text class="stat bold" y="12.5" data-testid="${id}">${label}:</text>
|
||||
<text
|
||||
class="stat"
|
||||
x="${hideProgress ? HIDDEN_PROGRESSBAR_PADDING : PROGRESSBAR_PADDING + progressBarWidth}"
|
||||
y="12.5"
|
||||
>${value}</text>
|
||||
${cardProgress}
|
||||
</g>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Recalculating percentages so that, compact layout's progress bar does not break when
|
||||
* hiding languages.
|
||||
*
|
||||
* @param {WakaTimeLang[]} languages The languages array.
|
||||
* @returns {void} The recalculated languages array.
|
||||
*/
|
||||
const recalculatePercentages = (languages) => {
|
||||
const totalSum = languages.reduce(
|
||||
(totalSum, language) => totalSum + language.percent,
|
||||
0,
|
||||
);
|
||||
const weight = +(100 / totalSum).toFixed(2);
|
||||
languages.forEach((language) => {
|
||||
language.percent = +(language.percent * weight).toFixed(2);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves CSS styles for a card.
|
||||
*
|
||||
* @param {Object} colors The colors to use for the card.
|
||||
* @param {string} colors.titleColor The title color.
|
||||
* @param {string} colors.textColor The text color.
|
||||
* @returns {string} Card CSS styles.
|
||||
*/
|
||||
const getStyles = ({
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
titleColor,
|
||||
textColor,
|
||||
}) => {
|
||||
return `
|
||||
.stat {
|
||||
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
|
||||
}
|
||||
@supports(-moz-appearance: auto) {
|
||||
/* Selector detects Firefox */
|
||||
.stat { font-size:12px; }
|
||||
}
|
||||
.stagger {
|
||||
opacity: 0;
|
||||
animation: fadeInAnimation 0.3s ease-in-out forwards;
|
||||
}
|
||||
.not_bold { font-weight: 400 }
|
||||
.bold { font-weight: 700 }
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize incoming width (string or number) and clamp to minimum.
|
||||
*
|
||||
* @param {Object} args The function arguments.
|
||||
* @param {WakaTimeOptions["layout"] | undefined} args.layout The incoming layout value.
|
||||
* @param {number|undefined} args.value The incoming width value.
|
||||
* @returns {number} The normalized width value.
|
||||
*/
|
||||
const normalizeCardWidth = ({ value, layout }) => {
|
||||
if (value === undefined || value === null || isNaN(value)) {
|
||||
return DEFAULT_CARD_WIDTH;
|
||||
}
|
||||
return Math.max(
|
||||
layout === "compact" ? COMPACT_LAYOUT_MIN_WIDTH : MIN_CARD_WIDTH,
|
||||
value,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {import('../fetchers/types').WakaTimeData} WakaTimeData
|
||||
* @typedef {import('./types').WakaTimeOptions} WakaTimeOptions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Renders WakaTime card.
|
||||
*
|
||||
* @param {Partial<WakaTimeData>} stats WakaTime stats.
|
||||
* @param {Partial<WakaTimeOptions>} options Card options.
|
||||
* @returns {string} WakaTime card SVG.
|
||||
*/
|
||||
const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
|
||||
let { languages = [] } = stats;
|
||||
const {
|
||||
hide_title = false,
|
||||
hide_border = false,
|
||||
card_width,
|
||||
hide,
|
||||
line_height = DEFAULT_LINE_HEIGHT,
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
theme = "default",
|
||||
hide_progress,
|
||||
custom_title,
|
||||
locale,
|
||||
layout,
|
||||
langs_count = languages.length,
|
||||
border_radius,
|
||||
border_color,
|
||||
display_format = "time",
|
||||
disable_animations,
|
||||
} = options;
|
||||
|
||||
const normalizedWidth = normalizeCardWidth({ value: card_width, layout });
|
||||
|
||||
const shouldHideLangs = Array.isArray(hide) && hide.length > 0;
|
||||
if (shouldHideLangs) {
|
||||
const languagesToHide = new Set(hide.map((lang) => lowercaseTrim(lang)));
|
||||
languages = languages.filter(
|
||||
(lang) => !languagesToHide.has(lowercaseTrim(lang.name)),
|
||||
);
|
||||
}
|
||||
|
||||
// Since the percentages are sorted in descending order, we can just
|
||||
// slice from the beginning without sorting.
|
||||
languages = languages.slice(0, langs_count);
|
||||
recalculatePercentages(languages);
|
||||
|
||||
const i18n = new I18n({
|
||||
locale,
|
||||
translations: wakatimeCardLocales,
|
||||
});
|
||||
|
||||
const lheight = parseInt(String(line_height), 10);
|
||||
|
||||
const langsCount = clampValue(langs_count, 1, langs_count);
|
||||
|
||||
// returns theme based colors with proper overrides and defaults
|
||||
const { titleColor, textColor, iconColor, bgColor, borderColor } =
|
||||
getCardColors({
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
});
|
||||
|
||||
const filteredLanguages = languages
|
||||
.filter((language) => language.hours || language.minutes)
|
||||
.slice(0, langsCount);
|
||||
|
||||
// Calculate the card height depending on how many items there are
|
||||
// but if rank circle is visible clamp the minimum height to `150`
|
||||
let height = Math.max(45 + (filteredLanguages.length + 1) * lheight, 150);
|
||||
|
||||
const cssStyles = getStyles({
|
||||
titleColor,
|
||||
textColor,
|
||||
});
|
||||
|
||||
let finalLayout = "";
|
||||
|
||||
// RENDER COMPACT LAYOUT
|
||||
if (layout === "compact") {
|
||||
const width = normalizedWidth - 5;
|
||||
height =
|
||||
90 + Math.round(filteredLanguages.length / 2) * DEFAULT_LINE_HEIGHT;
|
||||
|
||||
// progressOffset holds the previous language's width and used to offset the next language
|
||||
// so that we can stack them one after another, like this: [--][----][---]
|
||||
let progressOffset = 0;
|
||||
const compactProgressBar = filteredLanguages
|
||||
.map((language) => {
|
||||
const progress =
|
||||
((width - COMPACT_LAYOUT_PROGRESSBAR_PADDING) * language.percent) /
|
||||
100;
|
||||
|
||||
// @ts-ignore
|
||||
const languageColor = languageColors[language.name] || "#858585";
|
||||
|
||||
const output = `
|
||||
<rect
|
||||
mask="url(#rect-mask)"
|
||||
data-testid="lang-progress"
|
||||
x="${progressOffset}"
|
||||
y="0"
|
||||
width="${progress}"
|
||||
height="8"
|
||||
fill="${languageColor}"
|
||||
/>
|
||||
`;
|
||||
progressOffset += progress;
|
||||
return output;
|
||||
})
|
||||
.join("");
|
||||
|
||||
finalLayout = `
|
||||
<mask id="rect-mask">
|
||||
<rect x="${COMPACT_LAYOUT_PROGRESSBAR_PADDING}" y="0" width="${width - 2 * COMPACT_LAYOUT_PROGRESSBAR_PADDING}" height="8" fill="white" rx="5" />
|
||||
</mask>
|
||||
${compactProgressBar}
|
||||
${
|
||||
filteredLanguages.length
|
||||
? createLanguageTextNode({
|
||||
y: 25,
|
||||
langs: filteredLanguages,
|
||||
display_format,
|
||||
card_width: normalizedWidth,
|
||||
}).join("")
|
||||
: noCodingActivityNode({
|
||||
// @ts-ignore
|
||||
color: textColor,
|
||||
text: stats.is_coding_activity_visible
|
||||
? stats.is_other_usage_visible
|
||||
? i18n.t("wakatimecard.nocodingactivity")
|
||||
: i18n.t("wakatimecard.nocodedetails")
|
||||
: i18n.t("wakatimecard.notpublic"),
|
||||
})
|
||||
}
|
||||
`;
|
||||
} else {
|
||||
finalLayout = flexLayout({
|
||||
items: filteredLanguages.length
|
||||
? filteredLanguages.map((language, index) => {
|
||||
return createTextNode({
|
||||
id: language.name,
|
||||
label: language.name,
|
||||
value: formatLanguageValue({ display_format, lang: language }),
|
||||
index,
|
||||
percent: language.percent,
|
||||
// @ts-ignore
|
||||
progressBarColor: titleColor,
|
||||
// @ts-ignore
|
||||
progressBarBackgroundColor: textColor,
|
||||
hideProgress: hide_progress,
|
||||
progressBarWidth: normalizedWidth - TOTAL_TEXT_WIDTH,
|
||||
});
|
||||
})
|
||||
: [
|
||||
noCodingActivityNode({
|
||||
// @ts-ignore
|
||||
color: textColor,
|
||||
text: stats.is_coding_activity_visible
|
||||
? stats.is_other_usage_visible
|
||||
? i18n.t("wakatimecard.nocodingactivity")
|
||||
: i18n.t("wakatimecard.nocodedetails")
|
||||
: i18n.t("wakatimecard.notpublic"),
|
||||
}),
|
||||
],
|
||||
gap: lheight,
|
||||
direction: "column",
|
||||
}).join("");
|
||||
}
|
||||
|
||||
// Get title range text
|
||||
let titleText = i18n.t("wakatimecard.title");
|
||||
switch (stats.range) {
|
||||
case "last_7_days":
|
||||
titleText += ` (${i18n.t("wakatimecard.last7days")})`;
|
||||
break;
|
||||
case "last_year":
|
||||
titleText += ` (${i18n.t("wakatimecard.lastyear")})`;
|
||||
break;
|
||||
}
|
||||
|
||||
const card = new Card({
|
||||
customTitle: custom_title,
|
||||
defaultTitle: titleText,
|
||||
width: normalizedWidth,
|
||||
height,
|
||||
border_radius,
|
||||
colors: {
|
||||
titleColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
bgColor,
|
||||
borderColor,
|
||||
},
|
||||
});
|
||||
|
||||
if (disable_animations) {
|
||||
card.disableAnimations();
|
||||
}
|
||||
|
||||
card.setHideBorder(hide_border);
|
||||
card.setHideTitle(hide_title);
|
||||
card.setCSS(
|
||||
`
|
||||
${cssStyles}
|
||||
@keyframes slideInAnimation {
|
||||
from {
|
||||
width: 0;
|
||||
}
|
||||
to {
|
||||
width: calc(100%-100px);
|
||||
}
|
||||
}
|
||||
@keyframes growWidthAnimation {
|
||||
from {
|
||||
width: 0;
|
||||
}
|
||||
to {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
|
||||
#rect-mask rect{
|
||||
animation: slideInAnimation 1s ease-in-out forwards;
|
||||
}
|
||||
.lang-progress{
|
||||
animation: growWidthAnimation 0.6s ease-in-out forwards;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
return card.render(`
|
||||
<svg x="0" y="0" width="100%">
|
||||
${finalLayout}
|
||||
</svg>
|
||||
`);
|
||||
};
|
||||
|
||||
export { renderWakatimeCard };
|
||||
export default renderWakatimeCard;
|
||||
+13
-149
@@ -1,33 +1,12 @@
|
||||
// @ts-check
|
||||
|
||||
import { encodeHTML } from "./html.js";
|
||||
import { flexLayout } from "./render.js";
|
||||
const { FlexLayout } = require("../common/utils");
|
||||
const { getAnimations } = require("../getStyles");
|
||||
|
||||
class Card {
|
||||
/**
|
||||
* Creates a new card instance.
|
||||
*
|
||||
* @param {object} args Card arguments.
|
||||
* @param {number=} args.width Card width.
|
||||
* @param {number=} args.height Card height.
|
||||
* @param {number=} args.border_radius Card border radius.
|
||||
* @param {string=} args.customTitle Card custom title.
|
||||
* @param {string=} args.defaultTitle Card default title.
|
||||
* @param {string=} args.titlePrefixIcon Card title prefix icon.
|
||||
* @param {object} [args.colors={}] Card colors arguments.
|
||||
* @param {string=} args.colors.titleColor Card title color.
|
||||
* @param {string=} args.colors.textColor Card text color.
|
||||
* @param {string=} args.colors.iconColor Card icon color.
|
||||
* @param {string|string[]=} args.colors.bgColor Card background color.
|
||||
* @param {string=} args.colors.borderColor Card border color.
|
||||
*/
|
||||
constructor({
|
||||
width = 100,
|
||||
height = 100,
|
||||
border_radius = 4.5,
|
||||
colors = {},
|
||||
customTitle,
|
||||
defaultTitle = "",
|
||||
title = "",
|
||||
titlePrefixIcon,
|
||||
}) {
|
||||
this.width = width;
|
||||
@@ -36,63 +15,29 @@ class Card {
|
||||
this.hideBorder = false;
|
||||
this.hideTitle = false;
|
||||
|
||||
this.border_radius = border_radius;
|
||||
|
||||
// returns theme based colors with proper overrides and defaults
|
||||
this.colors = colors;
|
||||
this.title =
|
||||
customTitle === undefined
|
||||
? encodeHTML(defaultTitle)
|
||||
: encodeHTML(customTitle);
|
||||
|
||||
this.title = title;
|
||||
this.css = "";
|
||||
|
||||
this.paddingX = 25;
|
||||
this.paddingY = 35;
|
||||
this.titlePrefixIcon = titlePrefixIcon;
|
||||
this.animations = true;
|
||||
this.a11yTitle = "";
|
||||
this.a11yDesc = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
disableAnimations() {
|
||||
this.animations = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} props The props object.
|
||||
* @param {string} props.title Accessibility title.
|
||||
* @param {string} props.desc Accessibility description.
|
||||
* @returns {void}
|
||||
*/
|
||||
setAccessibilityLabel({ title, desc }) {
|
||||
this.a11yTitle = title;
|
||||
this.a11yDesc = desc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value The CSS to add to the card.
|
||||
* @returns {void}
|
||||
*/
|
||||
setCSS(value) {
|
||||
this.css = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} value Whether to hide the border or not.
|
||||
* @returns {void}
|
||||
*/
|
||||
setHideBorder(value) {
|
||||
this.hideBorder = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} value Whether to hide the title or not.
|
||||
* @returns {void}
|
||||
*/
|
||||
setHideTitle(value) {
|
||||
this.hideTitle = value;
|
||||
if (value) {
|
||||
@@ -100,17 +45,10 @@ class Card {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text The title to set.
|
||||
* @returns {void}
|
||||
*/
|
||||
setTitle(text) {
|
||||
this.title = text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The rendered card title.
|
||||
*/
|
||||
renderTitle() {
|
||||
const titleText = `
|
||||
<text
|
||||
@@ -139,72 +77,14 @@ class Card {
|
||||
data-testid="card-title"
|
||||
transform="translate(${this.paddingX}, ${this.paddingY})"
|
||||
>
|
||||
${flexLayout({
|
||||
items: [this.titlePrefixIcon ? prefixIcon : "", titleText],
|
||||
${FlexLayout({
|
||||
items: [this.titlePrefixIcon && prefixIcon, titleText],
|
||||
gap: 25,
|
||||
}).join("")}
|
||||
</g>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The rendered card gradient.
|
||||
*/
|
||||
renderGradient() {
|
||||
if (typeof this.colors.bgColor !== "object") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const gradients = this.colors.bgColor.slice(1);
|
||||
return typeof this.colors.bgColor === "object"
|
||||
? `
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="gradient"
|
||||
gradientTransform="rotate(${this.colors.bgColor[0]})"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
${gradients.map((grad, index) => {
|
||||
let offset = (index * 100) / (gradients.length - 1);
|
||||
return `<stop offset="${offset}%" stop-color="#${grad}" />`;
|
||||
})}
|
||||
</linearGradient>
|
||||
</defs>
|
||||
`
|
||||
: "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves css animations for a card.
|
||||
*
|
||||
* @returns {string} Animation css.
|
||||
*/
|
||||
getAnimations = () => {
|
||||
return `
|
||||
/* Animations */
|
||||
@keyframes scaleInAnimation {
|
||||
from {
|
||||
transform: translate(-5px, 5px) scale(0);
|
||||
}
|
||||
to {
|
||||
transform: translate(-5px, 5px) scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes fadeInAnimation {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} body The inner body of the card.
|
||||
* @returns {string} The rendered card.
|
||||
*/
|
||||
render(body) {
|
||||
return `
|
||||
<svg
|
||||
@@ -213,46 +93,31 @@ class Card {
|
||||
viewBox="0 0 ${this.width} ${this.height}"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
aria-labelledby="descId"
|
||||
>
|
||||
<title id="titleId">${this.a11yTitle}</title>
|
||||
<desc id="descId">${this.a11yDesc}</desc>
|
||||
<style>
|
||||
.header {
|
||||
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
|
||||
fill: ${this.colors.titleColor};
|
||||
animation: fadeInAnimation 0.8s ease-in-out forwards;
|
||||
}
|
||||
@supports(-moz-appearance: auto) {
|
||||
/* Selector detects Firefox */
|
||||
.header { font-size: 15.5px; }
|
||||
}
|
||||
${this.css}
|
||||
|
||||
${process.env.NODE_ENV === "test" ? "" : this.getAnimations()}
|
||||
${
|
||||
this.animations === false
|
||||
? `* { animation-duration: 0s !important; animation-delay: 0s !important; }`
|
||||
: ""
|
||||
process.env.NODE_ENV === "test" || !this.animations
|
||||
? ""
|
||||
: getAnimations()
|
||||
}
|
||||
</style>
|
||||
|
||||
${this.renderGradient()}
|
||||
|
||||
<rect
|
||||
data-testid="card-bg"
|
||||
x="0.5"
|
||||
y="0.5"
|
||||
rx="${this.border_radius}"
|
||||
rx="4.5"
|
||||
height="99%"
|
||||
stroke="${this.colors.borderColor}"
|
||||
stroke="#E4E2E2"
|
||||
width="${this.width - 1}"
|
||||
fill="${
|
||||
typeof this.colors.bgColor === "object"
|
||||
? "url(#gradient)"
|
||||
: this.colors.bgColor
|
||||
}"
|
||||
fill="${this.colors.bgColor}"
|
||||
stroke-opacity="${this.hideBorder ? 0 : 1}"
|
||||
/>
|
||||
|
||||
@@ -271,5 +136,4 @@ class Card {
|
||||
}
|
||||
}
|
||||
|
||||
export { Card };
|
||||
export default Card;
|
||||
module.exports = Card;
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
const FALLBACK_LOCALE = "en";
|
||||
|
||||
/**
|
||||
* I18n translation class.
|
||||
*/
|
||||
class I18n {
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param {Object} options Options.
|
||||
* @param {string=} options.locale Locale.
|
||||
* @param {any} options.translations Translations.
|
||||
*/
|
||||
constructor({ locale, translations }) {
|
||||
this.locale = locale || FALLBACK_LOCALE;
|
||||
this.translations = translations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation.
|
||||
*
|
||||
* @param {string} str String to translate.
|
||||
* @returns {string} Translated string.
|
||||
*/
|
||||
t(str) {
|
||||
if (!this.translations[str]) {
|
||||
throw new Error(`${str} Translation string not found`);
|
||||
}
|
||||
|
||||
if (!this.translations[str][this.locale]) {
|
||||
throw new Error(
|
||||
`'${str}' translation not found for locale '${this.locale}'`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.translations[str][this.locale];
|
||||
}
|
||||
}
|
||||
|
||||
export { I18n };
|
||||
export default I18n;
|
||||
@@ -1,69 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import { renderError } from "./render.js";
|
||||
import { blacklist } from "./blacklist.js";
|
||||
import { whitelist, gistWhitelist } from "./envs.js";
|
||||
|
||||
const NOT_WHITELISTED_USERNAME_MESSAGE = "This username is not whitelisted";
|
||||
const NOT_WHITELISTED_GIST_MESSAGE = "This gist ID is not whitelisted";
|
||||
const BLACKLISTED_MESSAGE = "This username is blacklisted";
|
||||
|
||||
/**
|
||||
* Guards access using whitelist/blacklist.
|
||||
*
|
||||
* @param {Object} args The parameters object.
|
||||
* @param {any} args.res The response object.
|
||||
* @param {string} args.id Resource identifier (username or gist id).
|
||||
* @param {"username"|"gist"|"wakatime"} args.type The type of identifier.
|
||||
* @param {{ title_color?: string, text_color?: string, bg_color?: string, border_color?: string, theme?: string }} args.colors Color options for the error card.
|
||||
* @returns {{ isPassed: boolean, result?: any }} The result object indicating success or failure.
|
||||
*/
|
||||
const guardAccess = ({ res, id, type, colors }) => {
|
||||
if (!["username", "gist", "wakatime"].includes(type)) {
|
||||
throw new Error(
|
||||
'Invalid type. Expected "username", "gist", or "wakatime".',
|
||||
);
|
||||
}
|
||||
|
||||
const currentWhitelist = type === "gist" ? gistWhitelist : whitelist;
|
||||
const notWhitelistedMsg =
|
||||
type === "gist"
|
||||
? NOT_WHITELISTED_GIST_MESSAGE
|
||||
: NOT_WHITELISTED_USERNAME_MESSAGE;
|
||||
|
||||
if (Array.isArray(currentWhitelist) && !currentWhitelist.includes(id)) {
|
||||
const result = res.send(
|
||||
renderError({
|
||||
message: notWhitelistedMsg,
|
||||
secondaryMessage: "Please deploy your own instance",
|
||||
renderOptions: {
|
||||
...colors,
|
||||
show_repo_link: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
return { isPassed: false, result };
|
||||
}
|
||||
|
||||
if (
|
||||
type === "username" &&
|
||||
currentWhitelist === undefined &&
|
||||
blacklist.includes(id)
|
||||
) {
|
||||
const result = res.send(
|
||||
renderError({
|
||||
message: BLACKLISTED_MESSAGE,
|
||||
secondaryMessage: "Please deploy your own instance",
|
||||
renderOptions: {
|
||||
...colors,
|
||||
show_repo_link: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
return { isPassed: false, result };
|
||||
}
|
||||
|
||||
return { isPassed: true };
|
||||
};
|
||||
|
||||
export { guardAccess };
|
||||
@@ -1,10 +0,0 @@
|
||||
const blacklist = [
|
||||
"renovate-bot",
|
||||
"technote-space",
|
||||
"sw-yx",
|
||||
"YourUsername",
|
||||
"[YourUsername]",
|
||||
];
|
||||
|
||||
export { blacklist };
|
||||
export default blacklist;
|
||||
@@ -1,153 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import { clampValue } from "./ops.js";
|
||||
|
||||
const MIN = 60;
|
||||
const HOUR = 60 * MIN;
|
||||
const DAY = 24 * HOUR;
|
||||
|
||||
/**
|
||||
* Common durations in seconds.
|
||||
*/
|
||||
const DURATIONS = {
|
||||
ONE_MINUTE: MIN,
|
||||
FIVE_MINUTES: 5 * MIN,
|
||||
TEN_MINUTES: 10 * MIN,
|
||||
FIFTEEN_MINUTES: 15 * MIN,
|
||||
THIRTY_MINUTES: 30 * MIN,
|
||||
|
||||
TWO_HOURS: 2 * HOUR,
|
||||
FOUR_HOURS: 4 * HOUR,
|
||||
SIX_HOURS: 6 * HOUR,
|
||||
EIGHT_HOURS: 8 * HOUR,
|
||||
TWELVE_HOURS: 12 * HOUR,
|
||||
|
||||
ONE_DAY: DAY,
|
||||
TWO_DAY: 2 * DAY,
|
||||
SIX_DAY: 6 * DAY,
|
||||
TEN_DAY: 10 * DAY,
|
||||
};
|
||||
|
||||
/**
|
||||
* Common cache TTL values in seconds.
|
||||
*/
|
||||
const CACHE_TTL = {
|
||||
STATS_CARD: {
|
||||
DEFAULT: DURATIONS.ONE_DAY,
|
||||
MIN: DURATIONS.TWELVE_HOURS,
|
||||
MAX: DURATIONS.TWO_DAY,
|
||||
},
|
||||
TOP_LANGS_CARD: {
|
||||
DEFAULT: DURATIONS.SIX_DAY,
|
||||
MIN: DURATIONS.TWO_DAY,
|
||||
MAX: DURATIONS.TEN_DAY,
|
||||
},
|
||||
PIN_CARD: {
|
||||
DEFAULT: DURATIONS.TEN_DAY,
|
||||
MIN: DURATIONS.ONE_DAY,
|
||||
MAX: DURATIONS.TEN_DAY,
|
||||
},
|
||||
GIST_CARD: {
|
||||
DEFAULT: DURATIONS.TWO_DAY,
|
||||
MIN: DURATIONS.ONE_DAY,
|
||||
MAX: DURATIONS.TEN_DAY,
|
||||
},
|
||||
WAKATIME_CARD: {
|
||||
DEFAULT: DURATIONS.ONE_DAY,
|
||||
MIN: DURATIONS.TWELVE_HOURS,
|
||||
MAX: DURATIONS.TWO_DAY,
|
||||
},
|
||||
ERROR: DURATIONS.TEN_MINUTES,
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves the cache seconds based on the requested, default, min, and max values.
|
||||
*
|
||||
* @param {Object} args The parameters object.
|
||||
* @param {number} args.requested The requested cache seconds.
|
||||
* @param {number} args.def The default cache seconds.
|
||||
* @param {number} args.min The minimum cache seconds.
|
||||
* @param {number} args.max The maximum cache seconds.
|
||||
* @returns {number} The resolved cache seconds.
|
||||
*/
|
||||
const resolveCacheSeconds = ({ requested, def, min, max }) => {
|
||||
let cacheSeconds = clampValue(isNaN(requested) ? def : requested, min, max);
|
||||
|
||||
if (process.env.CACHE_SECONDS) {
|
||||
const envCacheSeconds = parseInt(process.env.CACHE_SECONDS, 10);
|
||||
if (!isNaN(envCacheSeconds)) {
|
||||
cacheSeconds = envCacheSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
return cacheSeconds;
|
||||
};
|
||||
|
||||
/**
|
||||
* Disables caching by setting appropriate headers on the response object.
|
||||
*
|
||||
* @param {any} res The response object.
|
||||
*/
|
||||
const disableCaching = (res) => {
|
||||
// Disable caching for browsers, shared caches/CDNs, and GitHub Camo.
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
"no-cache, no-store, must-revalidate, max-age=0, s-maxage=0",
|
||||
);
|
||||
res.setHeader("Pragma", "no-cache");
|
||||
res.setHeader("Expires", "0");
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the Cache-Control headers on the response object.
|
||||
*
|
||||
* @param {any} res The response object.
|
||||
* @param {number} cacheSeconds The cache seconds to set in the headers.
|
||||
*/
|
||||
const setCacheHeaders = (res, cacheSeconds) => {
|
||||
if (cacheSeconds < 1 || process.env.NODE_ENV === "development") {
|
||||
disableCaching(res);
|
||||
return;
|
||||
}
|
||||
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
`max-age=${cacheSeconds}, ` +
|
||||
`s-maxage=${cacheSeconds}, ` +
|
||||
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the Cache-Control headers for error responses on the response object.
|
||||
*
|
||||
* @param {any} res The response object.
|
||||
*/
|
||||
const setErrorCacheHeaders = (res) => {
|
||||
const envCacheSeconds = process.env.CACHE_SECONDS
|
||||
? parseInt(process.env.CACHE_SECONDS, 10)
|
||||
: NaN;
|
||||
if (
|
||||
(!isNaN(envCacheSeconds) && envCacheSeconds < 1) ||
|
||||
process.env.NODE_ENV === "development"
|
||||
) {
|
||||
disableCaching(res);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use lower cache period for errors.
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
`max-age=${CACHE_TTL.ERROR}, ` +
|
||||
`s-maxage=${CACHE_TTL.ERROR}, ` +
|
||||
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
resolveCacheSeconds,
|
||||
setCacheHeaders,
|
||||
setErrorCacheHeaders,
|
||||
DURATIONS,
|
||||
CACHE_TTL,
|
||||
};
|
||||
@@ -1,144 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import { themes } from "../../themes/index.js";
|
||||
|
||||
/**
|
||||
* Checks if a string is a valid hex color.
|
||||
*
|
||||
* @param {string} hexColor String to check.
|
||||
* @returns {boolean} True if the given string is a valid hex color.
|
||||
*/
|
||||
const isValidHexColor = (hexColor) => {
|
||||
return new RegExp(
|
||||
/^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4})$/,
|
||||
).test(hexColor);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the given string is a valid gradient.
|
||||
*
|
||||
* @param {string[]} colors Array of colors.
|
||||
* @returns {boolean} True if the given string is a valid gradient.
|
||||
*/
|
||||
const isValidGradient = (colors) => {
|
||||
return (
|
||||
colors.length > 2 &&
|
||||
colors.slice(1).every((color) => isValidHexColor(color))
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves a gradient if color has more than one valid hex codes else a single color.
|
||||
*
|
||||
* @param {string} color The color to parse.
|
||||
* @param {string | string[]} fallbackColor The fallback color.
|
||||
* @returns {string | string[]} The gradient or color.
|
||||
*/
|
||||
const fallbackColor = (color, fallbackColor) => {
|
||||
let gradient = null;
|
||||
|
||||
let colors = color ? color.split(",") : [];
|
||||
if (colors.length > 1 && isValidGradient(colors)) {
|
||||
gradient = colors;
|
||||
}
|
||||
|
||||
return (
|
||||
(gradient ? gradient : isValidHexColor(color) && `#${color}`) ||
|
||||
fallbackColor
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Object containing card colors.
|
||||
* @typedef {{
|
||||
* titleColor: string;
|
||||
* iconColor: string;
|
||||
* textColor: string;
|
||||
* bgColor: string | string[];
|
||||
* borderColor: string;
|
||||
* ringColor: string;
|
||||
* }} CardColors
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns theme based colors with proper overrides and defaults.
|
||||
*
|
||||
* @param {Object} args Function arguments.
|
||||
* @param {string=} args.title_color Card title color.
|
||||
* @param {string=} args.text_color Card text color.
|
||||
* @param {string=} args.icon_color Card icon color.
|
||||
* @param {string=} args.bg_color Card background color.
|
||||
* @param {string=} args.border_color Card border color.
|
||||
* @param {string=} args.ring_color Card ring color.
|
||||
* @param {string=} args.theme Card theme.
|
||||
* @returns {CardColors} Card colors.
|
||||
*/
|
||||
const getCardColors = ({
|
||||
title_color,
|
||||
text_color,
|
||||
icon_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
ring_color,
|
||||
theme,
|
||||
}) => {
|
||||
const defaultTheme = themes["default"];
|
||||
const isThemeProvided = theme !== null && theme !== undefined;
|
||||
|
||||
// @ts-ignore
|
||||
const selectedTheme = isThemeProvided ? themes[theme] : defaultTheme;
|
||||
|
||||
const defaultBorderColor =
|
||||
"border_color" in selectedTheme
|
||||
? selectedTheme.border_color
|
||||
: // @ts-ignore
|
||||
defaultTheme.border_color;
|
||||
|
||||
// get the color provided by the user else the theme color
|
||||
// finally if both colors are invalid fallback to default theme
|
||||
const titleColor = fallbackColor(
|
||||
title_color || selectedTheme.title_color,
|
||||
"#" + defaultTheme.title_color,
|
||||
);
|
||||
|
||||
// get the color provided by the user else the theme color
|
||||
// finally if both colors are invalid we use the titleColor
|
||||
const ringColor = fallbackColor(
|
||||
// @ts-ignore
|
||||
ring_color || selectedTheme.ring_color,
|
||||
titleColor,
|
||||
);
|
||||
const iconColor = fallbackColor(
|
||||
icon_color || selectedTheme.icon_color,
|
||||
"#" + defaultTheme.icon_color,
|
||||
);
|
||||
const textColor = fallbackColor(
|
||||
text_color || selectedTheme.text_color,
|
||||
"#" + defaultTheme.text_color,
|
||||
);
|
||||
const bgColor = fallbackColor(
|
||||
bg_color || selectedTheme.bg_color,
|
||||
"#" + defaultTheme.bg_color,
|
||||
);
|
||||
|
||||
const borderColor = fallbackColor(
|
||||
border_color || defaultBorderColor,
|
||||
"#" + defaultBorderColor,
|
||||
);
|
||||
|
||||
if (
|
||||
typeof titleColor !== "string" ||
|
||||
typeof textColor !== "string" ||
|
||||
typeof ringColor !== "string" ||
|
||||
typeof iconColor !== "string" ||
|
||||
typeof borderColor !== "string"
|
||||
) {
|
||||
throw new Error(
|
||||
"Unexpected behavior, all colors except background should be string.",
|
||||
);
|
||||
}
|
||||
|
||||
return { titleColor, iconColor, textColor, bgColor, borderColor, ringColor };
|
||||
};
|
||||
|
||||
export { isValidHexColor, isValidGradient, getCardColors };
|
||||
@@ -1,15 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
const whitelist = process.env.WHITELIST
|
||||
? process.env.WHITELIST.split(",")
|
||||
: undefined;
|
||||
|
||||
const gistWhitelist = process.env.GIST_WHITELIST
|
||||
? process.env.GIST_WHITELIST.split(",")
|
||||
: undefined;
|
||||
|
||||
const excludeRepositories = process.env.EXCLUDE_REPO
|
||||
? process.env.EXCLUDE_REPO.split(",")
|
||||
: [];
|
||||
|
||||
export { whitelist, gistWhitelist, excludeRepositories };
|
||||
@@ -1,84 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @type {string} A general message to ask user to try again later.
|
||||
*/
|
||||
const TRY_AGAIN_LATER = "Please try again later";
|
||||
|
||||
/**
|
||||
* @type {Object<string, string>} A map of error types to secondary error messages.
|
||||
*/
|
||||
const SECONDARY_ERROR_MESSAGES = {
|
||||
MAX_RETRY:
|
||||
"You can deploy own instance or wait until public will be no longer limited",
|
||||
NO_TOKENS:
|
||||
"Please add an env variable called PAT_1 with your GitHub API token in vercel",
|
||||
USER_NOT_FOUND: "Make sure the provided username is not an organization",
|
||||
GRAPHQL_ERROR: TRY_AGAIN_LATER,
|
||||
GITHUB_REST_API_ERROR: TRY_AGAIN_LATER,
|
||||
WAKATIME_USER_NOT_FOUND: "Make sure you have a public WakaTime profile",
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom error class to handle custom GRS errors.
|
||||
*/
|
||||
class CustomError extends Error {
|
||||
/**
|
||||
* Custom error constructor.
|
||||
*
|
||||
* @param {string} message Error message.
|
||||
* @param {string} type Error type.
|
||||
*/
|
||||
constructor(message, type) {
|
||||
super(message);
|
||||
this.type = type;
|
||||
this.secondaryMessage = SECONDARY_ERROR_MESSAGES[type] || type;
|
||||
}
|
||||
|
||||
static MAX_RETRY = "MAX_RETRY";
|
||||
static NO_TOKENS = "NO_TOKENS";
|
||||
static USER_NOT_FOUND = "USER_NOT_FOUND";
|
||||
static GRAPHQL_ERROR = "GRAPHQL_ERROR";
|
||||
static GITHUB_REST_API_ERROR = "GITHUB_REST_API_ERROR";
|
||||
static WAKATIME_ERROR = "WAKATIME_ERROR";
|
||||
}
|
||||
|
||||
/**
|
||||
* Missing query parameter class.
|
||||
*/
|
||||
class MissingParamError extends Error {
|
||||
/**
|
||||
* Missing query parameter error constructor.
|
||||
*
|
||||
* @param {string[]} missedParams An array of missing parameters names.
|
||||
* @param {string=} secondaryMessage Optional secondary message to display.
|
||||
*/
|
||||
constructor(missedParams, secondaryMessage) {
|
||||
const msg = `Missing params ${missedParams
|
||||
.map((p) => `"${p}"`)
|
||||
.join(", ")} make sure you pass the parameters in URL`;
|
||||
super(msg);
|
||||
this.missedParams = missedParams;
|
||||
this.secondaryMessage = secondaryMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve secondary message from an error object.
|
||||
*
|
||||
* @param {Error} err The error object.
|
||||
* @returns {string|undefined} The secondary message if available, otherwise undefined.
|
||||
*/
|
||||
const retrieveSecondaryMessage = (err) => {
|
||||
return "secondaryMessage" in err && typeof err.secondaryMessage === "string"
|
||||
? err.secondaryMessage
|
||||
: undefined;
|
||||
};
|
||||
|
||||
export {
|
||||
CustomError,
|
||||
MissingParamError,
|
||||
SECONDARY_ERROR_MESSAGES,
|
||||
TRY_AGAIN_LATER,
|
||||
retrieveSecondaryMessage,
|
||||
};
|
||||
@@ -1,90 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import wrap from "word-wrap";
|
||||
import { encodeHTML } from "./html.js";
|
||||
|
||||
/**
|
||||
* Retrieves num with suffix k(thousands) precise to given decimal places.
|
||||
*
|
||||
* @param {number} num The number to format.
|
||||
* @param {number=} precision The number of decimal places to include.
|
||||
* @returns {string|number} The formatted number.
|
||||
*/
|
||||
const kFormatter = (num, precision) => {
|
||||
const abs = Math.abs(num);
|
||||
const sign = Math.sign(num);
|
||||
|
||||
if (typeof precision === "number" && !isNaN(precision)) {
|
||||
return (sign * (abs / 1000)).toFixed(precision) + "k";
|
||||
}
|
||||
|
||||
if (abs < 1000) {
|
||||
return sign * abs;
|
||||
}
|
||||
|
||||
return sign * parseFloat((abs / 1000).toFixed(1)) + "k";
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert bytes to a human-readable string representation.
|
||||
*
|
||||
* @param {number} bytes The number of bytes to convert.
|
||||
* @returns {string} The human-readable representation of bytes.
|
||||
* @throws {Error} If bytes is negative or too large.
|
||||
*/
|
||||
const formatBytes = (bytes) => {
|
||||
if (bytes < 0) {
|
||||
throw new Error("Bytes must be a non-negative number");
|
||||
}
|
||||
|
||||
if (bytes === 0) {
|
||||
return "0 B";
|
||||
}
|
||||
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
|
||||
const base = 1024;
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(base));
|
||||
|
||||
if (i >= sizes.length) {
|
||||
throw new Error("Bytes is too large to convert to a human-readable string");
|
||||
}
|
||||
|
||||
return `${(bytes / Math.pow(base, i)).toFixed(1)} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Split text over multiple lines based on the card width.
|
||||
*
|
||||
* @param {string} text Text to split.
|
||||
* @param {number} width Line width in number of characters.
|
||||
* @param {number} maxLines Maximum number of lines.
|
||||
* @returns {string[]} Array of lines.
|
||||
*/
|
||||
const wrapTextMultiline = (text, width = 59, maxLines = 3) => {
|
||||
const fullWidthComma = ",";
|
||||
const encoded = encodeHTML(text);
|
||||
const isChinese = encoded.includes(fullWidthComma);
|
||||
|
||||
let wrapped = [];
|
||||
|
||||
if (isChinese) {
|
||||
wrapped = encoded.split(fullWidthComma); // Chinese full punctuation
|
||||
} else {
|
||||
wrapped = wrap(encoded, {
|
||||
width,
|
||||
}).split("\n"); // Split wrapped lines to get an array of lines
|
||||
}
|
||||
|
||||
const lines = wrapped.map((line) => line.trim()).slice(0, maxLines); // Only consider maxLines lines
|
||||
|
||||
// Add "..." to the last line if the text exceeds maxLines
|
||||
if (wrapped.length > maxLines) {
|
||||
lines[maxLines - 1] += "...";
|
||||
}
|
||||
|
||||
// Remove empty lines if text fits in less than maxLines lines
|
||||
const multiLineText = lines.filter(Boolean);
|
||||
return multiLineText;
|
||||
};
|
||||
|
||||
export { kFormatter, formatBytes, wrapTextMultiline };
|
||||
@@ -1,19 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* Encode string as HTML.
|
||||
*
|
||||
* @see https://stackoverflow.com/a/48073476/10629172
|
||||
*
|
||||
* @param {string} str String to encode.
|
||||
* @returns {string} Encoded string.
|
||||
*/
|
||||
const encodeHTML = (str) => {
|
||||
return str
|
||||
.replace(/[\u00A0-\u9999<>&](?!#)/gim, (i) => {
|
||||
return "&#" + i.charCodeAt(0) + ";";
|
||||
})
|
||||
.replace(/\u0008/gim, "");
|
||||
};
|
||||
|
||||
export { encodeHTML };
|
||||
@@ -1,21 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import axios from "axios";
|
||||
|
||||
/**
|
||||
* Send GraphQL request to GitHub API.
|
||||
*
|
||||
* @param {import('axios').AxiosRequestConfig['data']} data Request data.
|
||||
* @param {import('axios').AxiosRequestConfig['headers']} headers Request headers.
|
||||
* @returns {Promise<any>} Request response.
|
||||
*/
|
||||
const request = (data, headers) => {
|
||||
return axios({
|
||||
url: "https://api.github.com/graphql",
|
||||
method: "post",
|
||||
headers,
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export { request };
|
||||
+1
-45
@@ -1,55 +1,11 @@
|
||||
// @ts-check
|
||||
|
||||
const icons = {
|
||||
star: `<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>`,
|
||||
commits: `<path fill-rule="evenodd" d="M1.643 3.143L.427 1.927A.25.25 0 000 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 00.177-.427L2.715 4.215a6.5 6.5 0 11-1.18 4.458.75.75 0 10-1.493.154 8.001 8.001 0 101.6-5.684zM7.75 4a.75.75 0 01.75.75v2.992l2.028.812a.75.75 0 01-.557 1.392l-2.5-1A.75.75 0 017 8.25v-3.5A.75.75 0 017.75 4z"/>`,
|
||||
prs: `<path fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"/>`,
|
||||
prs_merged: `<path fill-rule="evenodd" d="M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218ZM4.25 13.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm8.5-4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5ZM5 3.25a.75.75 0 1 0 0 .005V3.25Z" />`,
|
||||
prs_merged_percentage: `<path fill-rule="evenodd" d="M13.442 2.558a.625.625 0 0 1 0 .884l-10 10a.625.625 0 1 1-.884-.884l10-10a.625.625 0 0 1 .884 0zM4.5 6a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0 1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zm7 6a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0 1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z" />`,
|
||||
issues: `<path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"/>`,
|
||||
icon: `<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>`,
|
||||
contribs: `<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>`,
|
||||
fork: `<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path>`,
|
||||
reviews: `<path fill-rule="evenodd" d="M8 2c1.981 0 3.671.992 4.933 2.078 1.27 1.091 2.187 2.345 2.637 3.023a1.62 1.62 0 0 1 0 1.798c-.45.678-1.367 1.932-2.637 3.023C11.67 13.008 9.981 14 8 14c-1.981 0-3.671-.992-4.933-2.078C1.797 10.83.88 9.576.43 8.898a1.62 1.62 0 0 1 0-1.798c.45-.677 1.367-1.931 2.637-3.022C4.33 2.992 6.019 2 8 2ZM1.679 7.932a.12.12 0 0 0 0 .136c.411.622 1.241 1.75 2.366 2.717C5.176 11.758 6.527 12.5 8 12.5c1.473 0 2.825-.742 3.955-1.715 1.124-.967 1.954-2.096 2.366-2.717a.12.12 0 0 0 0-.136c-.412-.621-1.242-1.75-2.366-2.717C10.824 4.242 9.473 3.5 8 3.5c-1.473 0-2.825.742-3.955 1.715-1.124.967-1.954 2.096-2.366 2.717ZM8 10a2 2 0 1 1-.001-3.999A2 2 0 0 1 8 10Z"/>`,
|
||||
discussions_started: `<path fill-rule="evenodd" d="M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z" />`,
|
||||
discussions_answered: `<path fill-rule="evenodd" d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" />`,
|
||||
gist: `<path fill-rule="evenodd" d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25Zm7.47 3.97a.75.75 0 0 1 1.06 0l2 2a.75.75 0 0 1 0 1.06l-2 2a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L10.69 8 9.22 6.53a.75.75 0 0 1 0-1.06ZM6.78 6.53 5.31 8l1.47 1.47a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-2-2a.75.75 0 0 1 0-1.06l2-2a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z" />`,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get rank icon
|
||||
*
|
||||
* @param {string} rankIcon - The rank icon type.
|
||||
* @param {string} rankLevel - The rank level.
|
||||
* @param {number} percentile - The rank percentile.
|
||||
* @returns {string} - The SVG code of the rank icon
|
||||
*/
|
||||
const rankIcon = (rankIcon, rankLevel, percentile) => {
|
||||
switch (rankIcon) {
|
||||
case "github":
|
||||
return `
|
||||
<svg x="-38" y="-30" height="66" width="66" aria-hidden="true" viewBox="0 0 16 16" version="1.1" data-view-component="true" data-testid="github-rank-icon">
|
||||
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
|
||||
</svg>
|
||||
`;
|
||||
case "percentile":
|
||||
return `
|
||||
<text x="-5" y="-12" alignment-baseline="central" dominant-baseline="central" text-anchor="middle" data-testid="percentile-top-header" class="rank-percentile-header">
|
||||
Top
|
||||
</text>
|
||||
<text x="-5" y="12" alignment-baseline="central" dominant-baseline="central" text-anchor="middle" data-testid="percentile-rank-value" class="rank-percentile-text">
|
||||
${percentile.toFixed(1)}%
|
||||
</text>
|
||||
`;
|
||||
case "default":
|
||||
default:
|
||||
return `
|
||||
<text x="-5" y="3" alignment-baseline="central" dominant-baseline="central" text-anchor="middle" data-testid="level-rank-icon">
|
||||
${rankLevel}
|
||||
</text>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
export { icons, rankIcon };
|
||||
export default icons;
|
||||
module.exports = icons;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
export { blacklist } from "./blacklist.js";
|
||||
export { Card } from "./Card.js";
|
||||
export { I18n } from "./I18n.js";
|
||||
export { icons } from "./icons.js";
|
||||
export { retryer } from "./retryer.js";
|
||||
export {
|
||||
ERROR_CARD_LENGTH,
|
||||
renderError,
|
||||
flexLayout,
|
||||
measureText,
|
||||
} from "./render.js";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user