diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 71b71275..fa7228a6 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,6 +1,6 @@ # These are supported funding model platforms -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +github: [anuraghazra] # 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 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d3ecf0fa..5ed7d3c9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -5,11 +5,12 @@ title: "" labels: "" assignees: "" --- + **Describe the bug** A clear and concise description of what the bug is. -**Expected behavior** +**Expected behaviour** A clear and concise description of what you expected to happen. **Screenshots / Live demo link (paste the github-readme-stats link as markdown image)** @@ -18,9 +19,11 @@ If applicable, add screenshots to help explain your problem. **Additional context** Add any other context about the problem here. - diff --git a/.github/workflows/empty-issues-closer.yaml b/.github/workflows/empty-issues-closer.yaml new file mode 100644 index 00000000..5f050fe0 --- /dev/null +++ b/.github/workflows/empty-issues-closer.yaml @@ -0,0 +1,30 @@ +name: Close empty issues and templates +on: + issues: + types: + - reopened + - opened + - edited + +jobs: + closeEmptyIssuesAndTemplates: + name: Close empty issues + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 # NOTE: Retrieve issue templates. + - name: Run empty issues closer action + uses: rickstaa/empty-issues-closer-action@v1 + 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. diff --git a/.github/workflows/preview-theme.yml b/.github/workflows/preview-theme.yml index f102b59c..db7e291d 100644 --- a/.github/workflows/preview-theme.yml +++ b/.github/workflows/preview-theme.yml @@ -5,8 +5,10 @@ on: types: [opened, synchronize, reopened] branches: - master - - theme-preview-script + - theme_preview_script + paths: - "themes/index.js" + - "scripts/preview-theme.js" jobs: build: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0eb02215..0911168f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,21 +20,14 @@ jobs: with: 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 install npm run test + - name: Run Prettier + run: | + npm run format:check + - name: Code Coverage uses: codecov/codecov-action@v1 diff --git a/.github/workflows/top-issues-dashboard.yml b/.github/workflows/top-issues-dashboard.yml new file mode 100644 index 00000000..a9fca7f3 --- /dev/null +++ b/.github/workflows/top-issues-dashboard.yml @@ -0,0 +1,22 @@ +name: Update top issues dashboard +on: + schedule: + - cron: "0 0 */7 * *" + +jobs: + showAndLabelTopIssues: + name: Update top issues Dashboard. + runs-on: ubuntu-latest + steps: + - name: Run top issues action + uses: rickstaa/top-issues-action@v1 + env: + github_token: ${{ secrets.GITHUB_TOKEN }} + with: + label: false + dashboard: true + dashboard_show_total_reactions: true + top_issues: true + top_bugs: true + top_features: true + top_pull_requests: true diff --git a/.gitignore b/.gitignore index 1c96025a..835034e3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ package-lock.json coverage vercel_token +# IDE +.vscode +*.code-workspace diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04528df1..0d4b558a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,41 +2,50 @@ We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: -- Reporting a issue -- Discussing the current state of the code -- Submitting a fix -- Proposing new features -- Becoming a maintainer +- Reporting an 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`. -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! +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)** ## Local Development -To run & test github-readme-stats you need to follow few simple steps :- -_(make sure you already have a [vercel](https://vercel.com/) account)_ +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)_ -1. Install [Vercel CLI](https://vercel.com/download) -1. Fork the repository and clone the code to your local machine -1. Run `npm install` in the repository root -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_1" 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 +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. Open `vercel.json` and set the maxDuration to 10. +6. Create a `.env` file in the root of the directory. +7. In the .env file add a new variable named "PAT_1" with your [GitHub Personal Access Token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token). +8. Run the command "vercel dev" to start a development server at . ## Themes Contribution -GitHub Readme Stats supports custom theming and you can also contribute new themes! +GitHub Readme Stats supports custom theming, and you can also contribute new themes! -All you need to do is edit [themes/index.js](./themes/index.js) file and add your theme at the end of the file. +All you need to do is edit the [themes/index.js](./themes/index.js) file and add your theme at the end of the file. -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`, `border_color` +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`, `border_color` > 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. @@ -50,41 +59,41 @@ We use GitHub issues to track public bugs. Report a bug by [opening a new issue] ## Frequently Asked Questions (FAQs) -**Q:** How to hide jupyter Notebook? +**Q:** How to hide Jupyter Notebook? > **Ans:** &hide=jupyter%20notebook -**Q:** I could not figure out how to deploy on my own vercel instance +**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 +> - docs: +> - YT tutorial by codeSTACKr: **Q:** Language Card is incorrect -> **Ans:** Please read all the related issues / comments before opening any issues regarding language card stats: +> **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 private commits & we cannot access any other private info of any users, so it's not possible. only way is to deploy on your own instance & use your own PAT (Personal Access Token) +> **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. @@ -92,9 +101,9 @@ People _love_ thorough bug reports. I'm not even kidding. **Great Feature Requests** tend to have: -- 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. +- 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. ## License diff --git a/LICENSE b/LICENSE index 83b114d1..14d9ba34 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Anurag Hazra +Copyright (c) 2020 Anurag Hazra Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/api/index.js b/api/index.js index 0cda587e..2315ddda 100644 --- a/api/index.js +++ b/api/index.js @@ -17,6 +17,7 @@ module.exports = async (req, res) => { hide, hide_title, hide_border, + card_width, hide_rank, show_icons, count_private, @@ -25,17 +26,17 @@ module.exports = async (req, res) => { title_color, icon_color, text_color, + text_bold, bg_color, theme, cache_seconds, + exclude_repo, custom_title, locale, disable_animations, border_radius, border_color, } = req.query; - let stats; - res.setHeader("Content-Type", "image/svg+xml"); if (blacklist.includes(username)) { @@ -47,15 +48,16 @@ module.exports = async (req, res) => { } try { - stats = await fetchStats( + const stats = await fetchStats( username, parseBoolean(count_private), parseBoolean(include_all_commits), + parseArray(exclude_repo), ); const cacheSeconds = clampValue( - parseInt(cache_seconds || CONSTANTS.TWO_HOURS, 10), - CONSTANTS.TWO_HOURS, + parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10), + CONSTANTS.FOUR_HOURS, CONSTANTS.ONE_DAY, ); @@ -67,12 +69,14 @@ module.exports = async (req, res) => { 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), line_height, title_color, icon_color, text_color, + text_bold: parseBoolean(text_bold), bg_color, theme, custom_title, diff --git a/api/pin.js b/api/pin.js index 7fad6c09..f2761cb6 100644 --- a/api/pin.js +++ b/api/pin.js @@ -27,8 +27,6 @@ module.exports = async (req, res) => { border_color, } = req.query; - let repoData; - res.setHeader("Content-Type", "image/svg+xml"); if (blacklist.includes(username)) { @@ -40,20 +38,20 @@ module.exports = async (req, res) => { } try { - repoData = await fetchRepo(username, repo); + const repoData = await fetchRepo(username, repo); let cacheSeconds = clampValue( - parseInt(cache_seconds || CONSTANTS.TWO_HOURS, 10), - CONSTANTS.TWO_HOURS, + parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10), + CONSTANTS.FOUR_HOURS, 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; + 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.starCount; const forks = repoData.forkCount; const isBothOver1K = stars > 1000 && forks > 1000; const isBothUnder1 = stars < 1 && forks < 1; diff --git a/api/top-langs.js b/api/top-langs.js index 89306fa2..80802074 100644 --- a/api/top-langs.js +++ b/api/top-langs.js @@ -31,8 +31,6 @@ module.exports = async (req, res) => { border_radius, border_color, } = req.query; - let topLangs; - res.setHeader("Content-Type", "image/svg+xml"); if (blacklist.includes(username)) { @@ -44,15 +42,14 @@ module.exports = async (req, res) => { } try { - topLangs = await fetchTopLanguages( + const topLangs = await fetchTopLanguages( username, parseArray(exclude_repo), - parseArray(hide), ); const cacheSeconds = clampValue( - parseInt(cache_seconds || CONSTANTS.TWO_HOURS, 10), - CONSTANTS.TWO_HOURS, + parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10), + CONSTANTS.FOUR_HOURS, CONSTANTS.ONE_DAY, ); diff --git a/api/wakatime.js b/api/wakatime.js index 4bb8fca5..18b52189 100644 --- a/api/wakatime.js +++ b/api/wakatime.js @@ -3,9 +3,10 @@ const { renderError, parseBoolean, clampValue, + parseArray, CONSTANTS, - isLocaleAvailable, } = require("../src/common/utils"); +const { isLocaleAvailable } = require("../src/translations"); const { fetchWakatimeStats } = require("../src/fetchers/wakatime-fetcher"); const wakatimeCard = require("../src/cards/wakatime-card"); @@ -26,6 +27,7 @@ module.exports = async (req, res) => { locale, layout, langs_count, + hide, api_domain, range, border_radius, @@ -42,8 +44,8 @@ module.exports = async (req, res) => { const stats = await fetchWakatimeStats({ username, api_domain, range }); let cacheSeconds = clampValue( - parseInt(cache_seconds || CONSTANTS.TWO_HOURS, 10), - CONSTANTS.TWO_HOURS, + parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10), + CONSTANTS.FOUR_HOURS, CONSTANTS.ONE_DAY, ); @@ -58,6 +60,7 @@ module.exports = async (req, res) => { custom_title, hide_title: parseBoolean(hide_title), hide_border: parseBoolean(hide_border), + hide: parseArray(hide), line_height, title_color, icon_color, diff --git a/docs/readme_cn.md b/docs/readme_cn.md index 60a7d26c..17fd2b71 100644 --- a/docs/readme_cn.md +++ b/docs/readme_cn.md @@ -290,7 +290,7 @@ _注意:热门语言并不表示我的技能水平或类似的水平,它是 你通常无法将图片靠边显示。为此,您可以使用以下方法: -```md +```html @@ -325,7 +325,7 @@ _注意:热门语言并不表示我的技能水平或类似的水平,它是 1. 选择 `Import Git Repository` ![](https://files.catbox.moe/pqub9q.png) 1. 选择 root 并将所有内容保持不变,并且只需添加名为 PAT_1 的环境变量(如图所示),其中将包含一个个人访问令牌(PAT),你可以在[这里](https://github.com/settings/tokens/new)轻松创建(保留默认,并且只需要命名下,名字随便) - ![](https://files.catbox.moe/caem5b.png) + ![](https://files.catbox.moe/0ez4g7.png) 1. 点击 deploy,这就完成了,查看你的域名就可使用 API 了! diff --git a/docs/readme_de.md b/docs/readme_de.md index aa0769c3..55523fe3 100644 --- a/docs/readme_de.md +++ b/docs/readme_de.md @@ -71,7 +71,7 @@ # GitHub Statistiken-Karte Kopiere folgendes in deine Readme um die Statistiken zu benutzen. -Passe den Wert des URL-Parameters `?username=` so an, dass dort dein GitHub Nutzername steht. +Passe den Wert des URL-Parameters `?username=` so an, dass dort dein GitHub-Nutzername steht. ```md [![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats) @@ -91,7 +91,7 @@ Um eine spezifische Statistik auszublenden, kann dem Query-Parameter `?hide=` ei ### Symbole anzeigen -Um Symbole anzuzeigen kann der URL-Paramter `show_icons=true` wie folgt verwendet werden: +Um Symbole anzuzeigen kann der URL-Parameter `show_icons=true` wie folgt verwendet werden: ```md ![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true) @@ -139,11 +139,11 @@ Du kannst mehrere, mit Kommas separierte, Werte in der bg_color Option angeben, &bg_color=WINKEL,FARBE1,FARBE2,FARBE3...FARBE10 ``` -> Hinweis bzgl. des Caches: Wenn die Anzahl der Forks und Stars geringer als 1 Tsd ist, haben die Repo-Cards eine Standard-Cachezeit von 30 Minuten (1800 Sekunden), ansonsten beträgt diese 2 Stunden (7200 Sekunden). Außerdem ist der Cache auf ein Minimum von 30 Minuten und ein Maximum von 24 Stunden begrenzt. +> Hinweis bzgl. des Caches: Wenn die Anzahl der Forks und Stars geringer als 1 Tsd. ist, haben die Repo-Cards eine Standard-Cachezeit von 30 Minuten (1800 Sekunden), ansonsten beträgt diese 2 Stunden (7200 Sekunden). Außerdem ist der Cache auf ein Minimum von 30 Minuten und ein Maximum von 24 Stunden begrenzt. #### Exklusive Optionen der Statistiken-Karte: -- `hide` - Verbirgt die angegeben Elemente _(mit Komma abgegrenzte Werte)_ +- `hide` - Verbirgt die [angegeben Elemente](#verbergen-individueller-statistiken) _(mit Komma abgegrenzte Werte)_ - `hide_title` - _(Boolean)_ - `hide_rank` - _(Boolean)_ - `show_icons` - _(Boolean)_ @@ -170,12 +170,12 @@ Du kannst mehrere, mit Kommas separierte, Werte in der bg_color Option angeben, - `hide_title` - _(Boolean)_ - `line_height` - Legt die Zeilenhöhe des Texts fest _(Zahl)_ -- `hide_progress` - Verbirgt die Fortschrittanzeige und Prozentzahl _(Boolean)_ +- `hide_progress` - Verbirgt die Fortschrittsanzeige und Prozentzahl _(Boolean)_ - `custom_title` - Legt einen benutzerdefinierten Titel fest - `layout` - Wechselt zwischen zwei verschiedenen Layouts: `default` & `compact` - `langs_count` - Begrenzt die Anzahl der angezeigten Sprachen auf der Karte - `api_domain` - Legt eine benutzerdefinierte API Domain fest, z.B. für [Hakatime](https://github.com/mujx/hakatime) oder [Wakapi](https://github.com/muety/wakapi) -- `range` – Fragt eine eine Zeitspanne an, als die standardmäßig in WakaTime hinterlegte, z.B. `last_7_days`. Siehe [WakaTime API Dokumentation](https://wakatime.com/developers#stats). +- `range` – Fragt eine andere Zeitspanne an, als jene, welche standardmäßig in Wakatime hinterlegt ist. Zum Beispiel `last_7_days`. Siehe [WakaTime API Dokumentation](https://wakatime.com/developers#stats). --- @@ -188,7 +188,7 @@ Und Bääm! Du bist nicht mehr auf 6 angeheftete Repositories limitiert. ### Benutzung Füge diesen Code in deine Readme-Datei ein und passe die Links an. -Passe den Wert des URL-Parameters `?username=` so an, dass dort dein GitHub Nutzername steht. +Passe den Wert des URL-Parameters `?username=` so an, dass dort dein GitHub-Nutzername steht. Den Wert des URL-Parameters `?repo=` musst du so anpassen, dass dort der Namen deines Repositorys steht. Endpunkt: `api/pin?username=anuraghazra&repo=github-readme-stats` @@ -201,7 +201,7 @@ Endpunkt: `api/pin?username=anuraghazra&repo=github-readme-stats` [![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](https://github.com/anuraghazra/github-readme-stats) -Benutze die [show_owner](#anpassungenpersonalisierung) Variable, um den Nutzernamen des Repo Eigentümers anzuzeigen. +Benutze die [show_owner](#anpassungenpersonalisierung) Variable, um den Nutzernamen des Repository-Eigentümers anzuzeigen. [![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&show_owner=true)](https://github.com/anuraghazra/github-readme-stats) @@ -214,7 +214,7 @@ _HINWEIS: Die Top Programmiersprachen treffen keine Aussage über persönliche F ### Benutzung Füge diesen Code in deine Readme-Datei ein und passe die Links an. -Passe den Wert des URL-Parameters `?username=` so an, dass dort dein GitHub Nutzername steht. +Passe den Wert des URL-Parameters `?username=` so an, dass dort dein GitHub-Nutzername steht. Endpunkt: `api/top-langs?username=anuraghazra` @@ -232,7 +232,7 @@ Du kannst den `?hide=language1,language2` URL-Parameter benutzen, um einzelne Sp ### Kompaktes Sprachen-Karte Layout -Du kannst die `&layout=compact` Option nutzen, um das Karten Design zu ändern. +Du kannst die `&layout=compact` Option nutzen, um das Kartendesign zu ändern. ```md [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=compact)](https://github.com/anuraghazra/github-readme-stats) @@ -248,7 +248,7 @@ Du kannst die `&layout=compact` Option nutzen, um das Karten Design zu ändern. # Wakatime Wochen-Statistik -Ändere `?username=` in den eigenen [Wakatime](https://wakatime.com) Benutzernamen. +Ändere `?username=` in den eigenen [Wakatime](https://wakatime.com)-Benutzernamen. ```md [![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) @@ -312,7 +312,7 @@ Wähle Eines von den [Standard-Themes](#themes) Üblicherweise ist es in `.md`-Dateien nicht möglich Bilder nebeneinander anzuzeigen. Um dies zu ermöglichen, kannst du folgendes tun: -```md +```html @@ -325,7 +325,7 @@ Wähle Eines von den [Standard-Themes](#themes) #### [Schritt für Schritt YouTube Tutorial by @codeSTACKr](https://youtu.be/n6d4KHSKqGk?t=107) -Da die GitHub API nur 5 Tsd Aufrufe pro Stunde zulässt, kann es passieren, dass meine `https://github-readme-stats.vercel.app/api` dieses Limit erreicht. +Da die GitHub API nur 5 Tsd. 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-Knopf um loszulegen! Hinweis: Seit [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) sollte es möglich sein, mehr als 5 Tsd Aufrufe pro Stunde ohne Downtimes zu verkraften :D @@ -349,7 +349,7 @@ Hinweis: Seit [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) ![](https://files.catbox.moe/pqub9q.png) 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) ![](https://files.catbox.moe/0ez4g7.png) -1. Klicke deploy, und das wars. Besuche deine Domains um die API zu benutzen! +1. Klicke auf `Deploy`, und das wars. Besuche deine Domains um die API zu benutzen! ## :sparkling_heart: Unterstütze das Projekt diff --git a/docs/readme_es.md b/docs/readme_es.md index ae0cc830..5ddceec7 100644 --- a/docs/readme_es.md +++ b/docs/readme_es.md @@ -64,7 +64,7 @@ - [Tarjeta de estadísticas de GitHub](#tarjeta-de-estadísticas-de-github) - [Pins adicionales de GitHub](#pines-adicionales-de-github) -- [Top Languages Card](#tarjeta-de-lenguajes-principales) +- [Tarjeta de Lenguajes Principales](#tarjeta-de-lenguajes-principales) - [Wakatime Week Stats](#estadísticas-de-la-semana-de-wakatime) - [Temas](#temas) - [Personalización](#personalización) @@ -157,7 +157,7 @@ Puedes pasar mútliples valores separados por coma en la opción `bg_color` para #### Opciones exclusivas de la Tarjeta de Estadísticas: -- `hide` - Oculta de las estadísticas los elementos especificados _ (valores separados por comas) _ +- `hide` - Oculta de las estadísticas [los elementos especificados](#ocultar-estadísticas-individualmente) _(valores separados por comas)_ - `hide_title` - _(booleano)_ - `hide_rank` - _(booleano)_ - `show_icons` - _(booleano)_ @@ -351,7 +351,7 @@ Escoja cualquiera de los [temas por defecto](#themes) Por lo general, no podrás acomodar las imágenes una al lado de la otra. Para hacerlo, puede usar este enfoque: -```md +```html diff --git a/docs/readme_fr.md b/docs/readme_fr.md index d69c39b7..20996bd6 100644 --- a/docs/readme_fr.md +++ b/docs/readme_fr.md @@ -153,7 +153,7 @@ Vous pouvez fournir plusieurs valeurs (suivie d'une virgule) dans l'option bg_co #### Stats Card Exclusive Options: -- `hide` - Masquer les éléments spécifiés dans les statistiques _(Comma seperated values)_ +- `hide` - Masquer [les éléments spécifiés](#cacher-les-statistiques-individuelles) dans les statistiques _(Comma seperated values)_ - `hide_title` - Masquer le titre _(boolean)_ - `hide_rank` - Masquer le rang _(boolean)_ - `show_icons` - Afficher les icônes _(boolean)_ @@ -290,7 +290,7 @@ Choisissez parmi l'un des [thèmes par défaut](#themes) En général, vous ne pourrez pas mettre les images côte à côte. Pour ce faire, vous pouvez utiliser cette approche : -```md +```html diff --git a/docs/readme_it.md b/docs/readme_it.md index 8ba49435..1b2df96a 100644 --- a/docs/readme_it.md +++ b/docs/readme_it.md @@ -288,7 +288,7 @@ Scegli uno dei [temi di default](#themes) Per allineare le card una accanto all'altra, puoi adottare questo approccio: -```md +```html diff --git a/docs/readme_ja.md b/docs/readme_ja.md index 6e7e747f..b00c77a7 100644 --- a/docs/readme_ja.md +++ b/docs/readme_ja.md @@ -296,7 +296,7 @@ _NOTE: Top languages は、ユーザのスキルレベルを示すものでは 通常、画像を並べてレイアウトすることはできません。画像を並べるには、以下のような方法があります。 -```md +```html diff --git a/docs/readme_kr.md b/docs/readme_kr.md index 55851fd2..f5dfc9f2 100644 --- a/docs/readme_kr.md +++ b/docs/readme_kr.md @@ -367,7 +367,7 @@ _참고: 그럴땐, 이렇게 해보세요! -```md +```html diff --git a/docs/readme_nl.md b/docs/readme_nl.md index 788f2c8b..597f0c86 100644 --- a/docs/readme_nl.md +++ b/docs/readme_nl.md @@ -346,7 +346,7 @@ Kies uit de [standaard thema\'s](#themes) Meestal kan je de afbeeldingen niet naast elkaar zetten, op deze manier wel: -```md +```html diff --git a/docs/readme_np.md b/docs/readme_np.md index b8f92967..15bb34ba 100644 --- a/docs/readme_np.md +++ b/docs/readme_np.md @@ -340,7 +340,7 @@ Change the `?username=` value to your [Wakatime](https://wakatime.com) username. तपाइले इमेजलाई सइद बय सइद अलीग्न गर्न सक्नु हुदैन तेसैले येसरी गर्नु होस् : -```md +```html diff --git a/docs/readme_pt-BR.md b/docs/readme_pt-BR.md index 6542f52d..7f4814ce 100644 --- a/docs/readme_pt-BR.md +++ b/docs/readme_pt-BR.md @@ -27,7 +27,7 @@

- Ver demonstração + Ver demonstração · Reportar erros · @@ -147,7 +147,7 @@ Personalize a aparência do seu `Stats Card` ou `Repo Card` da maneira que desej #### 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_title` - Ocultar 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)_ @@ -295,7 +295,7 @@ Escolha entre um dos [temas predefinidos](#temas) Por padrão, você não poderá organizar as imagens lado a lado. Para fazer isso, você pode usar a seguinte abordagem: -```md +```html diff --git a/docs/readme_tr.md b/docs/readme_tr.md index 5a1c974e..35f32f2f 100644 --- a/docs/readme_tr.md +++ b/docs/readme_tr.md @@ -351,7 +351,7 @@ Endpoint: `api/top-langs?username=mustafacagri` Genellikle resimleri yan yana düzenleyemezsiniz. Bunu yapmak için şu yaklaşımı kullanabilirsiniz: -```md +```html diff --git a/package.json b/package.json index 0cb50dc1..be9e4354 100644 --- a/package.json +++ b/package.json @@ -7,27 +7,37 @@ "test": "jest --coverage", "test:watch": "jest --watch", "theme-readme-gen": "node scripts/generate-theme-doc", - "preview-theme": "node scripts/preview-theme" + "preview-theme": "node scripts/preview-theme", + "generate-langs-json": "node scripts/generate-langs-json", + "format": "./node_modules/.bin/prettier --write .", + "format:check": "./node_modules/.bin/prettier --check ." }, "author": "Anurag Hazra", "license": "MIT", + "engines": { + "node": ">=13" + }, "devDependencies": { "@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", + "@uppercod/css-to-object": "^1.1.1", "axios-mock-adapter": "^1.18.1", - "css-to-object": "^1.1.0", + "color-contrast-checker": "^2.1.0", + "hjson": "^3.2.2", "husky": "^4.2.5", "jest": "^26.1.0", - "parse-diff": "^0.7.0" + "js-yaml": "^4.1.0", + "lodash.snakecase": "^4.1.1", + "parse-diff": "^0.7.0", + "prettier": "^2.1.2" }, "dependencies": { + "axios": "^0.24.0", "dotenv": "^8.2.0", "emoji-name-map": "^1.2.8", "github-username-regex": "^1.0.0", - "prettier": "^2.1.2", "word-wrap": "^1.2.3" }, "husky": { diff --git a/readme.md b/readme.md index 068e3978..3ef40724 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,7 @@

GitHub Readme Stats

GitHub Readme Stats

-

Get dynamically generated GitHub stats on your readmes!

+

Get dynamically generated GitHub stats on your READMEs!

@@ -32,6 +32,8 @@ Report Bug · Request Feature + · + Ask Question

Français @@ -65,13 +67,13 @@ Give india logo -Are you considering to support the project by donating to me? Please DON'T!! +Are you considering supporting the project by donating? Please DON'T!! -Instead, Help India fight 2nd deadly wave of COVID, -Thousands of people are dying in India for lack of Oxygen & COVID related necessary infrastructure. +Instead, Help India fight the 2nd deadly wave of COVID-19. +Thousands of people are dying in India because of a lack of Oxygen & also COVID-related infrastructure. -Visit [https://indiafightscorona.giveindia.org](https://indiafightscorona.giveindia.org) and make a small donation to help us fight covid and overcome this crisis. -Your small help goes a long way. :heart: +Visit [https://indiafightscorona.giveindia.org](https://indiafightscorona.giveindia.org) and make a small donation to help us fight COVID and overcome this crisis. +A small donation goes a long way. :heart:

@@ -83,13 +85,18 @@ Your small help goes a long way. :heart: - [Wakatime Week Stats](#wakatime-week-stats) - [Themes](#themes) - [Customization](#customization) + - [Common Options](#common-options) + - [Stats Card Exclusive Options](#stats-card-exclusive-options) + - [Repo Card Exclusive Options](#repo-card-exclusive-options) + - [Language Card Exclusive Options](#language-card-exclusive-options) + - [Wakatime Card Exclusive Option](#wakatime-card-exclusive-options) - [Deploy Yourself](#deploy-on-your-own-vercel-instance) # GitHub Stats Card Copy-paste this into your markdown content, and that's it. Simple! -Change the `?username=` value to your GitHub's username. +Change the `?username=` value to your GitHub username. ```md [![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats) @@ -97,11 +104,11 @@ Change the `?username=` value to your GitHub's username. _Note: Available ranks are S+ (top 1%), S (top 25%), A++ (top 45%), A+ (top 60%), and B+ (everyone). The values are calculated by using the [cumulative distribution function](https://en.wikipedia.org/wiki/Cumulative_distribution_function) using commits, contributions, issues, stars, pull requests, followers, and owned repositories. -The implementation can be investigated at [src/calculateRank.js](./src/calculateRank.js)_ +The implementation can be investigated at [src/calculateRank.js](./src/calculateRank.js)._ ### Hiding individual stats -To hide any specific stats, you can pass a query parameter `?hide=` with comma-separated values. +To hide any specific stats, you can pass a query parameter `&hide=` with comma-separated values. > Options: `&hide=stars,commits,prs,issues,contribs` @@ -111,9 +118,9 @@ To hide any specific stats, you can pass a query parameter `?hide=` with comma-s ### Adding private contributions count to total commits count -You can add the count of all your private contributions to the total commits count by using the query parameter `?count_private=true`. +You can add the count of all your private contributions to the total commits count by using the query parameter `&count_private=true`. -_Note: If you are deploying this project yourself, the private contributions will be counted by default otherwise you need to chose to share your private contribution counts._ +_Note: If you are deploying this project yourself, the private contributions will be counted by default. Otherwise, you need to choose to share your private contribution counts._ > Options: `&count_private=true` @@ -133,13 +140,13 @@ To enable icons, you can pass `show_icons=true` in the query param, like so: With inbuilt themes, you can customize the look of the card without doing any [manual customization](#customization). -Use `?theme=THEME_NAME` parameter like so :- +Use `&theme=THEME_NAME` parameter like so :- ```md ![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=radical) ``` -#### All inbuilt themes :- +#### All inbuilt themes:- dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula @@ -160,13 +167,15 @@ You can customize the appearance of your `Stats Card` or `Repo Card` however you - `bg_color` - Card's background color _(hex color)_ **or** a gradient in the form of _angle,start,end_ - `hide_border` - Hides the card's border _(boolean)_ - `theme` - name of the theme, choose from [all available themes](./themes/README.md) -- `cache_seconds` - set the cache header manually _(min: 1800, max: 86400)_ +- `cache_seconds` - set the cache header manually _(min: 7200, max: 86400)_ - `locale` - set the language in the card _(e.g. cn, de, es, etc.)_ -- `border_radius` - Corner rounding on the card_ +- `border_radius` - Corner rounding on the card + +> Note: The minimum of cache_seconds is currently 4 hours as a temporary fix for PATs exhaustion. ##### Gradient in bg_color -You can provide multiple comma-separated values in bg_color option to render a gradient, the format of the gradient is :- +You can provide multiple comma-separated values in the bg_color option to render a gradient, with the following format: ``` &bg_color=DEG,COLOR1,COLOR2,COLOR3...COLOR10 @@ -176,14 +185,17 @@ You can provide multiple comma-separated values in bg_color option to render a g #### Stats Card Exclusive Options: -- `hide` - Hides the specified items from stats _(Comma-separated values)_ +- `hide` - Hides the [specified items](#hiding-individual-stats) from stats _(Comma-separated values)_ - `hide_title` - _(boolean)_ +- `card_width` - Set the card's width manually _(number)_ - `hide_rank` - _(boolean)_ hides the rank and automatically resizes the card width - `show_icons` - _(boolean)_ - `include_all_commits` - Count total commits instead of just the current year commits _(boolean)_ - `count_private` - Count private commits _(boolean)_ - `line_height` - Sets the line-height between text _(number)_ +- `exclude_repo` - Exclude stars from specified repositories _(Comma-separated values)_ - `custom_title` - Sets a custom title for the card +- `text_bold` - Use bold text _(boolean)_ - `disable_animations` - Disables all animations in the card _(boolean)_ #### Repo Card Exclusive Options: @@ -207,14 +219,15 @@ You can provide multiple comma-separated values in bg_color option to render a g #### Wakatime Card Exclusive Options: +- `hide` - Hide the languages specified from the card _(Comma-separated values)_ - `hide_title` - _(boolean)_ - `line_height` - Sets the line-height between text _(number)_ - `hide_progress` - Hides the progress bar and percentage _(boolean)_ - `custom_title` - Sets a custom title for the card - `layout` - Switch between two available layouts `default` & `compact` -- `langs_count` - Limit number of languages on the card, defaults to all reported langauges +- `langs_count` - Limit the number of languages on the card, defaults to all reported languages - `api_domain` - Set a custom API domain for the card, e.g. to use services like [Hakatime](https://github.com/mujx/hakatime) or [Wakapi](https://github.com/muety/wakapi) -- `range` – Request a range different from your WakaTime default, e.g. `last_7_days`. See [WakaTime API docs](https://wakatime.com/developers#stats) for list of available options. +- `range` – Request a range different from your WakaTime default, e.g. `last_7_days`. See [WakaTime API docs](https://wakatime.com/developers#stats) for a list of available options. --- @@ -244,9 +257,9 @@ Use [show_owner](#customization) variable to include the repo's owner username # Top Languages Card -The top languages card show a GitHub user's most frequently used top language. +The top languages card shows a GitHub user's most frequently used top language. -_NOTE: Top Languages does not indicate my skill level or anything like that, it's a GitHub metric of which languages have the most code on GitHub. It's a new feature of github-readme-stats._ +_NOTE: Top Languages does not indicate my skill level or anything like that; it's a GitHub metric to determine which languages have the most code on GitHub. It's a new feature of github-readme-stats._ ### Usage @@ -260,7 +273,7 @@ Endpoint: `api/top-langs?username=anuraghazra` ### Exclude individual repositories -You can use `?exclude_repo=repo1,repo2` parameter to exclude individual repositories. +You can use `&exclude_repo=repo1,repo2` parameter to exclude individual repositories. ```md [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&exclude_repo=github-readme-stats,anuraghazra.github.io)](https://github.com/anuraghazra/github-readme-stats) @@ -268,7 +281,7 @@ You can use `?exclude_repo=repo1,repo2` parameter to exclude individual reposito ### Hide individual languages -You can use `?hide=language1,language2` parameter to hide individual languages. +You can use `&hide=language1,language2` parameter to hide individual languages. ```md [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&hide=javascript,html)](https://github.com/anuraghazra/github-readme-stats) @@ -306,6 +319,8 @@ Change the `?username=` value to your [Wakatime](https://wakatime.com) username. [![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) ``` +> Note: Please be aware that we currently only show data from Wakatime profiles that are public. + ### Demo [![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) @@ -403,14 +418,16 @@ NOTE: Since [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) we ![](https://files.catbox.moe/tct1wg.png) 1. Sign in with GitHub by pressing `Continue with GitHub` ![](https://files.catbox.moe/btd78j.jpeg) -1. Sign into GitHub and allow access to all repositories, if prompted +1. Sign in to GitHub and allow access to all repositories, if prompted 1. Fork this repo +1. After forking the repo, open the [`vercel.json`](https://github.com/anuraghazra/github-readme-stats/blob/master/vercel.json#L5) file and change the `maxDuration` field to `10` 1. Go back to your [Vercel dashboard](https://vercel.com/dashboard) 1. Select `Import Project` ![](https://files.catbox.moe/qckos0.png) -1. Select `Import Git Repository` +1. Select `Import Git Repository`. Select root and keep everything as is. ![](https://files.catbox.moe/pqub9q.png) -1. Select root and keep everything as is, just add your environment variable named PAT_1 (as shown), which will contain a personal access token (PAT), which you can easily create [here](https://github.com/settings/tokens/new) (leave everything as is, just name it something, it can be anything you want) +1. Create a personal access token (PAT) [here](https://github.com/settings/tokens/new) and enable the `repo` permissions (this allows access to see private repo stats) +1. Add the PAT as an environment variable named `PAT_1` (as shown). ![](https://files.catbox.moe/0ez4g7.png) 1. Click deploy, and you're good to go. See your domains to use the API! @@ -421,7 +438,7 @@ NOTE: Since [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) we I open-source almost everything I can, and I try to reply to everyone needing help using these projects. Obviously, this takes time. You can use this service for free. -However, if you are using this project and are happy with it or just want to encourage me to continue creating stuff, there are few ways you can do it :- +However, if you are using this project and are happy with it or just want to encourage me to continue creating stuff, there are a few ways you can do it:- - Giving proper credit when you use github-readme-stats on your readme, linking back to it :D - Starring and sharing the project :rocket: @@ -437,3 +454,4 @@ Thanks! :heart: Contributions are welcome! <3 Made with :heart: and JavaScript. + diff --git a/scripts/generate-langs-json.js b/scripts/generate-langs-json.js new file mode 100644 index 00000000..bd210a3c --- /dev/null +++ b/scripts/generate-langs-json.js @@ -0,0 +1,30 @@ +const fs = require("fs"); +const jsYaml = require("js-yaml"); +const axios = require("axios"); + +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, " "), + ); + }); diff --git a/scripts/generate-theme-doc.js b/scripts/generate-theme-doc.js index 806b2cc3..fcb3c5ee 100644 --- a/scripts/generate-theme-doc.js +++ b/scripts/generate-theme-doc.js @@ -12,7 +12,7 @@ const THEME_TEMPLATE = `## Available Themes -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 :- @@ -43,7 +43,7 @@ ${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 +Want to add a new theme? Consider reading the [contribution guidelines](../CONTRIBUTING.md#themes-contribution) :D `; const createRepoMdLink = (theme) => { diff --git a/scripts/preview-theme.js b/scripts/preview-theme.js index e809777b..e4422ecd 100644 --- a/scripts/preview-theme.js +++ b/scripts/preview-theme.js @@ -1,8 +1,16 @@ const core = require("@actions/core"); const github = require("@actions/github"); const parse = require("parse-diff"); +const Hjson = require("hjson"); +const snakeCase = require("lodash.snakecase"); +const ColorContrastChecker = require("color-contrast-checker"); + require("dotenv").config(); +const OWNER = "anuraghazra"; +const REPO = "github-readme-stats"; +const COMMENT_TITLE = "Automated Theme Preview"; + function getPrNumber() { const pullRequest = github.context.payload.pull_request; if (!pullRequest) { @@ -12,17 +20,72 @@ function getPrNumber() { return pullRequest.number; } +function findCommentPredicate(inputs, comment) { + return ( + (inputs.commentAuthor && comment.user + ? comment.user.login === inputs.commentAuthor + : true) && + (inputs.bodyIncludes && comment.body + ? comment.body.includes(inputs.bodyIncludes) + : true) + ); +} + +async function findComment(octokit, issueNumber) { + const parameters = { + owner: OWNER, + repo: REPO, + issue_number: issueNumber, + }; + const inputs = { + commentAuthor: OWNER, + bodyIncludes: COMMENT_TITLE, + }; + + for await (const { data: comments } of octokit.paginate.iterator( + octokit.rest.issues.listComments, + parameters, + )) { + // Search each page for the comment + const comment = comments.find((comment) => + findCommentPredicate(inputs, comment), + ); + if (comment) return comment; + } +} + +async function upsertComment(octokit, props) { + if (props.comment_id !== undefined) { + await octokit.issues.updateComment(props); + } else { + await octokit.issues.createComment(props); + } +} + +function getWebAimLink(color1, color2) { + return `https://webaim.org/resources/contrastchecker/?fcolor=${color1}&bcolor=${color2}`; +} + +function 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`; +} + const themeContribGuidelines = ` - \r> Hi thanks for the theme contribution, please read our theme contribution 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). + \rWe are currently only accepting color combinations from any VSCode theme or themes which have good color combination to minimize bloating the themes collection. - \r> We are currently only accepting color combinations from any vscode theme or which has good color combination to minimize bloating the themes collection. - - \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) - \r> Read our [contribution guidelines](https://github.com/anuraghazra/github-readme-stats/blob/master/CONTRIBUTING.md) for more info + \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) `; async function run() { try { + const ccc = new ColorContrastChecker(); + const warnings = []; const token = core.getInput("token"); const octokit = github.getOctokit(token || process.env.PERSONAL_TOKEN); const pullRequestId = getPrNumber(); @@ -32,65 +95,90 @@ async function run() { return; } - let res = await octokit.pulls.get({ - owner: "anuraghazra", - repo: "github-readme-stats", + const res = await octokit.pulls.get({ + owner: OWNER, + repo: REPO, pull_number: pullRequestId, mediaType: { format: "diff", }, }); + const comment = await findComment(octokit, pullRequestId); - let diff = parse(res.data); - let colorStrings = diff + const diff = parse(res.data); + const content = diff .find((file) => file.to === "themes/index.js") .chunks[0].changes.filter((c) => c.type === "add") .map((c) => c.content.replace("+", "")) .join(""); - let matches = colorStrings.match(/(title_color:.*bg_color.*\")/); - let colors = matches && matches[0].split(","); + const themeObject = Hjson.parse(content); + const themeName = Object.keys(themeObject)[0]; + const colors = themeObject[themeName]; + + if (themeName !== snakeCase(themeName)) { + warnings.push("Theme name isn't in snake_case"); + } if (!colors) { - await octokit.issues.createComment({ - owner: "anuraghazra", - repo: "github-readme-stats", + await upsertComment({ + comment_id: comment?.id, + owner: OWNER, + repo: REPO, + issue_number: pullRequestId, body: ` - \rTheme preview (bot) + \r**${COMMENT_TITLE}** \rCannot create theme preview ${themeContribGuidelines} `, - issue_number: pullRequestId, }); return; } - colors = colors.map((color) => - color.replace(/.*\:\s/, "").replace(/\"/g, ""), - ); - const titleColor = colors[0]; - const iconColor = colors[1]; - const textColor = colors[2]; - const 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`; + const titleColor = colors.title_color; + const iconColor = colors.icon_color; + const textColor = colors.text_color; + const bgColor = colors.bg_color; + const url = getGrsLink(colors); - await octokit.issues.createComment({ - owner: "anuraghazra", - repo: "github-readme-stats", + const colorPairs = { + title_color: [titleColor, bgColor], + icon_color: [iconColor, bgColor], + text_color: [textColor, bgColor], + }; + + // check color contrast + Object.keys(colorPairs).forEach((key) => { + const color1 = colorPairs[key][0]; + const color2 = colorPairs[key][1]; + if (!ccc.isLevelAA(`#${color1}`, `#${color2}`)) { + const permalink = getWebAimLink(color1, color2); + warnings.push( + `\`${key}\` does not pass [AA contrast ratio](${permalink})`, + ); + } + }); + + await upsertComment(octokit, { + comment_id: comment?.id, + issue_number: pullRequestId, + owner: OWNER, + repo: REPO, body: ` - \rTheme preview (bot) + \r**${COMMENT_TITLE}** + \r${warnings.map((warning) => `- :warning: ${warning}\n`).join("")} + \ntitle_color: #${titleColor} | icon_color: #${iconColor} | text_color: #${textColor} | bg_color: #${bgColor} - \rLink: ${url} + \r[Preview Link](${url}) \r[![](${url})](${url}) ${themeContribGuidelines} `, - issue_number: pullRequestId, }); } catch (error) { console.log(error); diff --git a/src/calculateRank.js b/src/calculateRank.js index df6fdcd3..a835e5dd 100644 --- a/src/calculateRank.js +++ b/src/calculateRank.js @@ -36,20 +36,14 @@ function calculateRank({ const RANK_A = 0.50; const RANK_B_PLUS = 0.75; - let level = ""; - - if (rank <= RANK_S_PLUS) - level = "S+"; - else if (rank <= RANK_S) - level = "S"; - else if (rank <= RANK_A_PLUS) - level = "A+"; - else if (rank <= RANK_A) - level = "A"; - else if (rank <= RANK_B_PLUS) - level = "B+"; - else - level = "B"; + const level = (() => { + if (rank <= RANK_S_PLUS) return "S+"; + if (rank <= RANK_S) return "S"; + if (rank <= RANK_A_PLUS) return "A+"; + if (rank <= RANK_A) return "A"; + if (rank <= RANK_B_PLUS) return "B+"; + return "B"; + })(); return {level, score: rank * 100}; } diff --git a/src/cards/repo-card.js b/src/cards/repo-card.js index 9a21fcbd..034ee1f6 100644 --- a/src/cards/repo-card.js +++ b/src/cards/repo-card.js @@ -1,25 +1,85 @@ -const toEmoji = require("emoji-name-map"); +// @ts-check const { kFormatter, encodeHTML, getCardColors, flexLayout, wrapTextMultiline, + measureText, + parseEmojis, } = require("../common/utils"); const I18n = require("../common/I18n"); const Card = require("../common/Card"); const icons = require("../common/icons"); const { repoCardLocales } = require("../translations"); +/** + * @param {string} label + * @param {string} textColor + * @returns {string} + */ +const getBadgeSVG = (label, textColor) => ` + + + + ${label} + + +`; + +/** + * @param {string} langName + * @param {string} langColor + * @returns {string} + */ +const createLanguageNode = (langName, langColor) => { + return ` + + + ${langName} + + `; +}; + +const ICON_SIZE = 16; +const iconWithLabel = (icon, label, testid) => { + if (label <= 0) return ""; + const iconSvg = ` + + ${icon} + + `; + const text = `${label}`; + return flexLayout({ items: [iconSvg, text], gap: 20 }).join(""); +}; + +/** + * @param {import('../fetchers/types').RepositoryData} repo + * @param {Partial} options + * @returns {string} + */ const renderRepoCard = (repo, options = {}) => { const { name, nameWithOwner, description, primaryLanguage, - stargazers, isArchived, isTemplate, + starCount, forkCount, } = repo; const { @@ -35,22 +95,17 @@ const renderRepoCard = (repo, options = {}) => { locale, } = options; + const lineHeight = 10; 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 desc = parseEmojis(description || "No description provided"); const multiLineDescription = wrapTextMultiline(desc); const descriptionLines = multiLineDescription.length; - const lineHeight = 10; + const descriptionSvg = multiLineDescription + .map((line) => `${encodeHTML(line)}`) + .join(""); const height = (descriptionLines > 1 ? 120 : 110) + descriptionLines * lineHeight; @@ -61,13 +116,7 @@ const renderRepoCard = (repo, options = {}) => { }); // returns theme based colors with proper overrides and defaults - const { - titleColor, - textColor, - iconColor, - bgColor, - borderColor, - } = getCardColors({ + const colors = getCardColors({ title_color, icon_color, text_color, @@ -76,74 +125,41 @@ const renderRepoCard = (repo, options = {}) => { theme, }); - const totalStars = kFormatter(stargazers.totalCount); - const totalForks = kFormatter(forkCount); - - const getBadgeSVG = (label) => ` - - - - ${label} - - - `; - const svgLanguage = primaryLanguage - ? ` - - - ${langName} - - ` + ? createLanguageNode(langName, langColor) : ""; - const iconWithLabel = (icon, label, testid) => { - return ` - - ${icon} - - ${label} - `; - }; - const svgStars = - stargazers.totalCount > 0 && - iconWithLabel(icons.star, totalStars, "stargazers"); - const svgForks = - forkCount > 0 && iconWithLabel(icons.fork, totalForks, "forkcount"); + const totalStars = kFormatter(starCount); + const totalForks = kFormatter(forkCount); + const svgStars = iconWithLabel(icons.star, totalStars, "stargazers"); + const svgForks = iconWithLabel(icons.fork, totalForks, "forkcount"); const starAndForkCount = flexLayout({ - items: [svgStars, svgForks], - gap: 65, + 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, + defaultTitle: header.length > 35 ? `${header.slice(0, 35)}...` : header, titlePrefixIcon: icons.contribs, width: 400, height, border_radius, - colors: { - titleColor, - textColor, - iconColor, - bgColor, - borderColor, - }, + colors, }); card.disableAnimations(); card.setHideBorder(hide_border); 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} } + .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 } `); @@ -151,27 +167,20 @@ const renderRepoCard = (repo, options = {}) => { return card.render(` ${ isTemplate - ? getBadgeSVG(i18n.t("repocard.template")) + ? // @ts-ignore + getBadgeSVG(i18n.t("repocard.template"), colors.textColor) : isArchived - ? getBadgeSVG(i18n.t("repocard.archived")) + ? // @ts-ignore + getBadgeSVG(i18n.t("repocard.archived"), colors.textColor) : "" } - ${multiLineDescription - .map((line) => `${encodeHTML(line)}`) - .join("")} + ${descriptionSvg} - - ${svgLanguage} - - - ${starAndForkCount} - + + ${starAndForkCount} `); }; diff --git a/src/cards/stats-card.js b/src/cards/stats-card.js index 99582e0a..30056b85 100644 --- a/src/cards/stats-card.js +++ b/src/cards/stats-card.js @@ -1,3 +1,4 @@ +// @ts-check const I18n = require("../common/I18n"); const Card = require("../common/Card"); const icons = require("../common/icons"); @@ -19,6 +20,7 @@ const createTextNode = ({ index, showIcons, shiftValuePos, + bold, }) => { const kValue = kFormatter(value); const staggerDelay = (index + 3) * 150; @@ -34,17 +36,24 @@ const createTextNode = ({ return ` ${iconSvg} - ${label}: - ${label}: + ${kValue} `; }; +/** + * @param {Partial} stats + * @param {Partial} options + * @returns {string} + */ const renderStatsCard = (stats = {}, options = { hide: [] }) => { const { name, @@ -60,12 +69,14 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => { show_icons = false, hide_title = false, hide_border = false, + card_width, hide_rank = false, include_all_commits = false, line_height = 25, title_color, icon_color, text_color, + text_bold = true, bg_color, theme = "default", custom_title, @@ -75,23 +86,18 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => { disable_animations = false, } = options; - const lheight = parseInt(line_height, 10); + const lheight = parseInt(String(line_height), 10); // 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 { titleColor, textColor, iconColor, bgColor, borderColor } = + getCardColors({ + title_color, + icon_color, + text_color, + bg_color, + border_color, + theme, + }); const apostrophe = ["x", "s"].includes(name.slice(-1).toLocaleLowerCase()) ? "" @@ -147,6 +153,8 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => { "id", "my", "pl", + "de", + "nl", ]; const isLongLocale = longLocales.includes(locale) === true; @@ -160,7 +168,8 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => { index, showIcons: show_icons, shiftValuePos: - (!include_all_commits ? 50 : 20) + (isLongLocale ? 50 : 0), + (!include_all_commits ? 50 : 35) + (isLongLocale ? 50 : 0), + bold: text_bold, }), ); @@ -171,26 +180,6 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => { hide_rank ? 0 : 150, ); - // Conditionally rendered elements - const rankCircle = hide_rank - ? "" - : ` - - - - - ${rank.level} - - - `; - // 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; @@ -206,13 +195,20 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => { return measureText(custom_title ? custom_title : i18n.t("statcard.title")); }; - const width = hide_rank - ? clampValue( - 50 /* padding */ + calculateTextWidth() * 2, - 270 /* min */, - Infinity, - ) - : 495; + /* + 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 ? 16 : 0; + const minCardWidth = hide_rank + ? clampValue(50 /* padding */ + calculateTextWidth() * 2, 270, Infinity) + : 340 + iconWidth; + const defaultCardWidth = hide_rank ? 270 : 495; + let width = isNaN(card_width) ? defaultCardWidth : card_width; + if (width < minCardWidth) { + width = minCardWidth; + } const card = new Card({ customTitle: custom_title, @@ -235,16 +231,72 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => { if (disable_animations) card.disableAnimations(); + /** + * Calculates the right rank circle translation values such that the rank circle + * keeps respecting the padding. + * + * width > 450: The default left padding of 50 px will be used. + * width < 450: The left and right padding will shrink equally. + * + * @returns {number} - Rank circle translation value. + */ + const calculateRankXTranslation = () => { + if (width < 450) { + return width - 95 + (45 * (450 - 340)) / 110; + } else { + return width - 95; + } + }; + + // Conditionally rendered elements + const rankCircle = hide_rank + ? "" + : ` + + + + + ${rank.level} + + + `; + + // Accessibility Labels + const labels = Object.keys(STATS) + .filter((key) => !hide.includes(key)) + .map((key) => { + if (key === "commits") { + return `${i18n.t("statcard.commits")} ${ + include_all_commits ? "" : `in ${new Date().getFullYear()}` + } : ${totalStars}`; + } + return `${STATS[key].label}: ${STATS[key].value}`; + }) + .join(", "); + + card.setAccessibilityLabel({ + title: `${card.title}, Rank: ${rank.level}`, + desc: labels, + }); + return card.render(` ${rankCircle} - ${flexLayout({ items: statItems, gap: lheight, direction: "column", }).join("")} - + `); }; diff --git a/src/cards/top-languages-card.js b/src/cards/top-languages-card.js index f281b125..b2f5c362 100644 --- a/src/cards/top-languages-card.js +++ b/src/cards/top-languages-card.js @@ -1,16 +1,45 @@ +// @ts-check const Card = require("../common/Card"); const I18n = require("../common/I18n"); const { langCardLocales } = require("../translations"); const { createProgressNode } = require("../common/createProgressNode"); -const { clampValue, getCardColors, flexLayout } = require("../common/utils"); +const { + clampValue, + getCardColors, + flexLayout, + lowercaseTrim, + measureText, + chunkArray, +} = require("../common/utils"); const DEFAULT_CARD_WIDTH = 300; +const MIN_CARD_WIDTH = 230; const DEFAULT_LANGS_COUNT = 5; const DEFAULT_LANG_COLOR = "#858585"; const CARD_PADDING = 25; -const lowercaseTrim = (name) => name.toLowerCase().trim(); +/** + * @typedef {import("../fetchers/types").Lang} Lang + */ +/** + * @param {Lang[]} arr + */ +const getLongestLang = (arr) => + arr.reduce( + (savedLang, lang) => + lang.name.length > savedLang.name.length ? lang : savedLang, + { name: "", size: null, color: "" }, + ); + +/** + * @param {{ + * width: number, + * color: string, + * name: string, + * progress: string + * }} props + */ const createProgressTextNode = ({ width, color, name, progress }) => { const paddingRight = 95; const progressTextX = width - paddingRight + 10; @@ -30,12 +59,15 @@ const createProgressTextNode = ({ width, color, name, progress }) => { `; }; -const createCompactLangNode = ({ lang, totalSize, x, y }) => { +/** + * @param {{ lang: Lang, totalSize: number }} props + */ +const createCompactLangNode = ({ lang, totalSize }) => { const percentage = ((lang.size / totalSize) * 100).toFixed(2); const color = lang.color || "#858585"; return ` - + ${lang.name} ${percentage}% @@ -44,30 +76,40 @@ const createCompactLangNode = ({ lang, totalSize, x, y }) => { `; }; -const createLanguageTextNode = ({ langs, totalSize, x, y }) => { - return langs.map((lang, index) => { - if (index % 2 === 0) { - return createCompactLangNode({ +/** + * @param {{ langs: Lang[], totalSize: number }} props + */ +const createLanguageTextNode = ({ langs, totalSize }) => { + 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, - x, - y: 12.5 * index + y, totalSize, + // @ts-ignore index, - }); - } - return createCompactLangNode({ - lang, - x: 150, - y: 12.5 + 12.5 * index, - totalSize, - 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(""); }; /** - * - * @param {any[]} langs + * @param {Lang[]} langs * @param {number} width * @param {number} totalLanguageSize * @returns {string} @@ -88,8 +130,7 @@ const renderNormalLayout = (langs, width, totalLanguageSize) => { }; /** - * - * @param {any[]} langs + * @param {Lang[]} langs * @param {number} width * @param {number} totalLanguageSize * @returns {string} @@ -129,12 +170,13 @@ const renderCompactLayout = (langs, width, totalLanguageSize) => { ${compactProgressBar} - ${createLanguageTextNode({ - x: 0, - y: 25, - langs, - totalSize: totalLanguageSize, - }).join("")} + + + ${createLanguageTextNode({ + langs, + totalSize: totalLanguageSize, + })} + `; }; @@ -154,6 +196,12 @@ const calculateNormalLayoutHeight = (totalLangs) => { return 45 + (totalLangs + 1) * 40; }; +/** + * + * @param {Record} topLangs + * @param {string[]} hide + * @param {string} langs_count + */ const useLanguages = (topLangs, hide, langs_count) => { let langs = Object.values(topLangs); let langsToHide = {}; @@ -167,7 +215,7 @@ const useLanguages = (topLangs, hide, langs_count) => { }); } - // filter out langauges to be hidden + // filter out languages to be hidden langs = langs .sort((a, b) => b.size - a.size) .filter((lang) => { @@ -180,6 +228,11 @@ const useLanguages = (topLangs, hide, langs_count) => { return { langs, totalLanguageSize }; }; +/** + * @param {import('../fetchers/types').TopLangData} topLangs + * @param {Partial} options + * @returns {string} + */ const renderTopLanguages = (topLangs, options = {}) => { const { hide_title, @@ -206,10 +259,14 @@ const renderTopLanguages = (topLangs, options = {}) => { const { langs, totalLanguageSize } = useLanguages( topLangs, hide, - langs_count, + String(langs_count), ); - let width = isNaN(card_width) ? DEFAULT_CARD_WIDTH : card_width; + let width = isNaN(card_width) + ? DEFAULT_CARD_WIDTH + : card_width < MIN_CARD_WIDTH + ? MIN_CARD_WIDTH + : card_width; let height = calculateNormalLayoutHeight(langs.length); let finalLayout = ""; @@ -255,3 +312,4 @@ const renderTopLanguages = (topLangs, options = {}) => { }; module.exports = renderTopLanguages; +module.exports.MIN_CARD_WIDTH = MIN_CARD_WIDTH; diff --git a/src/cards/types.d.ts b/src/cards/types.d.ts new file mode 100644 index 00000000..46bf6659 --- /dev/null +++ b/src/cards/types.d.ts @@ -0,0 +1,51 @@ +type ThemeNames = keyof typeof import("../../themes"); + +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; +}; + +export type StatCardOptions = CommonOptions & { + hide: string[]; + show_icons: boolean; + hide_title: boolean; + hide_border: boolean; + card_width: number; + hide_rank: boolean; + include_all_commits: boolean; + line_height: number | string; + custom_title: string; + disable_animations: boolean; +}; + +export type RepoCardOptions = CommonOptions & { + hide_border: boolean; + show_owner: boolean; +}; + +export type TopLangOptions = CommonOptions & { + hide_title: boolean; + hide_border: boolean; + card_width: number; + hide: string[]; + layout: "compact" | "normal"; + custom_title: string; + langs_count: number; +}; + +type WakaTimeOptions = CommonOptions & { + hide_title: boolean; + hide_border: boolean; + hide: string[]; + line_height: string; + hide_progress: boolean; + custom_title: string; + layout: "compact" | "normal"; + langs_count: number; +}; diff --git a/src/cards/wakatime-card.js b/src/cards/wakatime-card.js index 5e12cf39..5ab2f29d 100644 --- a/src/cards/wakatime-card.js +++ b/src/cards/wakatime-card.js @@ -1,17 +1,35 @@ +// @ts-check const Card = require("../common/Card"); const I18n = require("../common/I18n"); const { getStyles } = require("../getStyles"); const { wakatimeCardLocales } = require("../translations"); const languageColors = require("../common/languageColors.json"); const { createProgressNode } = require("../common/createProgressNode"); -const { clampValue, getCardColors, flexLayout } = require("../common/utils"); +const { + clampValue, + getCardColors, + flexLayout, + lowercaseTrim, +} = require("../common/utils"); +/** + * @param {{color: string, text: string}} param0 + */ const noCodingActivityNode = ({ color, text }) => { return ` ${text} `; }; +/** + * + * @param {{ + * lang: import("../fetchers/types").WakaTimeLang, + * totalSize: number, + * x: number, + * y: number + * }} props + */ const createCompactLangNode = ({ lang, totalSize, x, y }) => { const color = languageColors[lang.name] || "#858585"; @@ -25,6 +43,14 @@ const createCompactLangNode = ({ lang, totalSize, x, y }) => { `; }; +/** + * @param {{ + * langs: import("../fetchers/types").WakaTimeLang[], + * totalSize: number, + * x: number, + * y: number + * }} props + */ const createLanguageTextNode = ({ langs, totalSize, x, y }) => { return langs.map((lang, index) => { if (index % 2 === 0) { @@ -33,7 +59,6 @@ const createLanguageTextNode = ({ langs, totalSize, x, y }) => { x: 25, y: 12.5 * index + y, totalSize, - index, }); } return createCompactLangNode({ @@ -41,11 +66,23 @@ const createLanguageTextNode = ({ langs, totalSize, x, y }) => { x: 230, y: 12.5 + 12.5 * index, totalSize, - index, }); }); }; +/** + * + * @param {{ + * id: string; + * label: string; + * value: string; + * index: number; + * percent: number; + * hideProgress: boolean; + * progressBarColor: string; + * progressBarBackgroundColor: string + * }} props + */ const createTextNode = ({ id, label, @@ -61,34 +98,56 @@ const createTextNode = ({ const cardProgress = hideProgress ? null : createProgressNode({ - x: 110, - y: 4, - progress: percent, - color: progressBarColor, - width: 220, - name: label, - progressBarBackgroundColor, - }); + x: 110, + y: 4, + progress: percent, + color: progressBarColor, + width: 220, + // @ts-ignore + name: label, + progressBarBackgroundColor, + }); return ` - ${label}: + ${label}: ${value} ${cardProgress} `; }; +/** + * @param {import("../fetchers/types").WakaTimeLang[]} languages + */ +const recalculatePercentages = (languages) => { + // recalculating percentages so that, + // compact layout's progress bar does not break when hiding 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); + }); +}; + +/** + * @param {Partial} stats + * @param {Partial} options + * @returns {string} + */ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { - const { languages } = stats; + let { languages } = stats; const { hide_title = false, hide_border = false, + hide, line_height = 25, title_color, icon_color, @@ -104,35 +163,39 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { border_color, } = options; + const shouldHideLangs = Array.isArray(hide) && hide.length > 0; + if (shouldHideLangs && languages !== undefined) { + const languagesToHide = new Set(hide.map((lang) => lowercaseTrim(lang))); + languages = languages.filter( + (lang) => !languagesToHide.has(lowercaseTrim(lang.name)), + ); + recalculatePercentages(languages); + } + const i18n = new I18n({ locale, translations: wakatimeCardLocales, }); - const lheight = parseInt(line_height, 10); + const lheight = parseInt(String(line_height), 10); - langsCount = clampValue(parseInt(langs_count), 1, langs_count); + const langsCount = clampValue(parseInt(String(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 { titleColor, textColor, iconColor, bgColor, borderColor } = + getCardColors({ + title_color, + icon_color, + text_color, + bg_color, + border_color, + theme, + }); const filteredLanguages = languages ? languages - .filter((language) => language.hours || language.minutes) - .slice(0, langsCount) + .filter((language) => language.hours || language.minutes) + .slice(0, langsCount) : []; // Calculate the card height depending on how many items there are @@ -186,33 +249,35 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { ${compactProgressBar} ${createLanguageTextNode({ - x: 0, - y: 25, - langs: filteredLanguages, - totalSize: 100, - }).join("")} + x: 0, + y: 25, + langs: filteredLanguages, + totalSize: 100, + }).join("")} `; } else { finalLayout = flexLayout({ items: filteredLanguages.length - ? filteredLanguages - .map((language) => { + ? filteredLanguages.map((language) => { return createTextNode({ id: language.name, label: language.name, value: language.text, percent: language.percent, + // @ts-ignore progressBarColor: titleColor, + // @ts-ignore progressBarBackgroundColor: textColor, hideProgress: hide_progress, }); }) : [ - noCodingActivityNode({ - color: textColor, - text: i18n.t("wakatimecard.nocodingactivity"), - }), - ], + noCodingActivityNode({ + // @ts-ignore + color: textColor, + text: i18n.t("wakatimecard.nocodingactivity"), + }), + ], gap: lheight, direction: "column", }).join(""); diff --git a/src/common/Card.js b/src/common/Card.js index 520560f9..2cc84553 100644 --- a/src/common/Card.js +++ b/src/common/Card.js @@ -2,6 +2,16 @@ const { getAnimations } = require("../getStyles"); const { flexLayout, encodeHTML } = require("../common/utils"); class Card { + /** + * @param {object} args + * @param {number?=} args.width + * @param {number?=} args.height + * @param {number?=} args.border_radius + * @param {string?=} args.customTitle + * @param {string?=} args.defaultTitle + * @param {string?=} args.titlePrefixIcon + * @param {ReturnType?=} args.colors + */ constructor({ width = 100, height = 100, @@ -32,20 +42,39 @@ class Card { this.paddingY = 35; this.titlePrefixIcon = titlePrefixIcon; this.animations = true; + this.a11yTitle = ""; + this.a11yDesc = ""; } disableAnimations() { this.animations = false; } + /** + * @param {{title: string, desc: string}} prop + */ + setAccessibilityLabel({ title, desc }) { + this.a11yTitle = title; + this.a11yDesc = desc; + } + + /** + * @param {string} value + */ setCSS(value) { this.css = value; } + /** + * @param {boolean} value + */ setHideBorder(value) { this.hideBorder = value; } + /** + * @param {boolean} value + */ setHideTitle(value) { this.hideTitle = value; if (value) { @@ -53,6 +82,9 @@ class Card { } } + /** + * @param {string} text + */ setTitle(text) { this.title = text; } @@ -101,8 +133,9 @@ class Card { ? ` ${gradients.map((grad, index) => { let offset = (index * 100) / (gradients.length - 1); @@ -114,6 +147,9 @@ class Card { : ""; } + /** + * @param {string} body + */ render(body) { return ` + ${this.a11yTitle} + ${this.a11yDesc} - Something went wrong! file an issue at https://git.io/JJmN9 + Something went wrong! file an issue at https://tiny.one/readme-stats ${encodeHTML(message)} ${secondaryMessage} @@ -20,7 +27,11 @@ const renderError = (message, secondaryMessage = "") => { `; }; -// https://stackoverflow.com/a/48073476/10629172 +/** + * @see https://stackoverflow.com/a/48073476/10629172 + * @param {string} str + * @returns {string} + */ function encodeHTML(str) { return str .replace(/[\u00A0-\u9999<>&](?!#)/gim, (i) => { @@ -29,18 +40,29 @@ function encodeHTML(str) { .replace(/\u0008/gim, ""); } +/** + * @param {number} num + */ function kFormatter(num) { return Math.abs(num) > 999 - ? Math.sign(num) * (Math.abs(num) / 1000).toFixed(1) + "k" + ? Math.sign(num) * parseFloat((Math.abs(num) / 1000).toFixed(1)) + "k" : Math.sign(num) * Math.abs(num); } +/** + * @param {string} hexColor + * @returns {boolean} + */ function 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); } +/** + * @param {string} value + * @returns {boolean | string} + */ function parseBoolean(value) { if (value === "true") { return true; @@ -51,19 +73,37 @@ function parseBoolean(value) { } } +/** + * @param {string} str + */ function parseArray(str) { if (!str) return []; return str.split(","); } +/** + * @param {number} number + * @param {number} min + * @param {number} max + */ function clampValue(number, min, max) { + // @ts-ignore + if (Number.isNaN(parseInt(number))) return min; return Math.max(min, Math.min(number, max)); } +/** + * @param {string[]} colors + */ function isValidGradient(colors) { return isValidHexColor(colors[1]) && isValidHexColor(colors[2]); } +/** + * @param {string} color + * @param {string} fallbackColor + * @returns {string | string[]} + */ function fallbackColor(color, fallbackColor) { let colors = color.split(","); let gradient = null; @@ -78,7 +118,12 @@ function fallbackColor(color, fallbackColor) { ); } +/** + * @param {import('axios').AxiosRequestConfig['data']} data + * @param {import('axios').AxiosRequestConfig['headers']} headers + */ function request(data, headers) { + // @ts-ignore return axios({ url: "https://api.github.com/graphql", method: "post", @@ -88,34 +133,53 @@ function request(data, headers) { } /** + * @param {object} props + * @param {string[]} props.items + * @param {number} props.gap + * @param {number[]?=} props.sizes + * @param {"column" | "row"?=} props.direction * - * @param {String[]} items - * @param {Number} gap - * @param {string} direction + * @returns {string[]} * * @description * Auto layout utility, allows us to layout things * vertically or horizontally with proper gaping */ -function flexLayout({ items, gap, direction }) { +function flexLayout({ items, gap, direction, sizes = [] }) { + let lastSize = 0; // filter() for filtering out empty strings return items.filter(Boolean).map((item, i) => { - let transform = `translate(${gap * i}, 0)`; + const size = sizes[i] || 0; + let transform = `translate(${lastSize}, 0)`; if (direction === "column") { - transform = `translate(0, ${gap * i})`; + transform = `translate(0, ${lastSize})`; } + lastSize += size + gap; return `${item}`; }); } -// returns theme based colors with proper overrides and defaults +/** + * @typedef {object} CardColors + * @prop {string?=} title_color + * @prop {string?=} text_color + * @prop {string?=} icon_color + * @prop {string?=} bg_color + * @prop {string?=} border_color + * @prop {keyof typeof import('../../themes')?=} fallbackTheme + * @prop {keyof typeof import('../../themes')?=} theme + */ +/** + * returns theme based colors with proper overrides and defaults + * @param {CardColors} options + */ function getCardColors({ title_color, text_color, icon_color, bg_color, - theme, border_color, + theme, fallbackTheme = "default", }) { const defaultTheme = themes[fallbackTheme]; @@ -150,12 +214,28 @@ function getCardColors({ return { titleColor, iconColor, textColor, bgColor, borderColor }; } -function wrapTextMultiline(text, width = 60, maxLines = 3) { - const wrapped = wrap(encodeHTML(text), { width }) - .split("\n") // Split wrapped lines to get an array of lines - .map((line) => line.trim()); // Remove leading and trailing whitespace of each line +/** + * @param {string} text + * @param {number} width + * @param {number} maxLines + * @returns {string[]} + */ +function wrapTextMultiline(text, width = 59, maxLines = 3) { + const fullWidthComma = ","; + const encoded = encodeHTML(text); + const isChinese = encoded.includes(fullWidthComma); - const lines = wrapped.slice(0, maxLines); // Only consider maxLines lines + 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) { @@ -186,17 +266,41 @@ const SECONDARY_ERROR_MESSAGES = { }; class CustomError extends Error { + /** + * @param {string} message + * @param {string} type + */ constructor(message, type) { super(message); this.type = type; - this.secondaryMessage = SECONDARY_ERROR_MESSAGES[type] || "adsad"; + this.secondaryMessage = SECONDARY_ERROR_MESSAGES[type] || type; } static MAX_RETRY = "MAX_RETRY"; static USER_NOT_FOUND = "USER_NOT_FOUND"; } -// https://stackoverflow.com/a/48172630/10629172 +class MissingParamError extends Error { + /** + * @param {string[]} missedParams + * @param {string?=} secondaryMessage + */ + 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; + } +} + +/** + * @see https://stackoverflow.com/a/48172630/10629172 + * @param {string} str + * @param {number} fontSize + * @returns + */ function measureText(str, fontSize = 10) { // prettier-ignore const widths = [ @@ -231,6 +335,41 @@ function measureText(str, fontSize = 10) { ); } +/** @param {string} name */ +const lowercaseTrim = (name) => name.toLowerCase().trim(); + +/** + * @template T + * @param {Array} arr + * @param {number} perChunk + * @returns {Array} + */ +function chunkArray(arr, perChunk) { + return arr.reduce((resultArray, item, index) => { + const chunkIndex = Math.floor(index / perChunk); + + if (!resultArray[chunkIndex]) { + resultArray[chunkIndex] = []; // start a new chunk + } + + resultArray[chunkIndex].push(item); + + return resultArray; + }, []); +} + +/** + * + * @param {string} str + * @returns {string} + */ +function parseEmojis(str) { + if (!str) throw new Error("[parseEmoji]: str argument not provided"); + return str.replace(/:\w+:/gm, (emoji) => { + return toEmoji.get(emoji) || ""; + }); +} + module.exports = { renderError, kFormatter, @@ -248,4 +387,8 @@ module.exports = { logger, CONSTANTS, CustomError, + MissingParamError, + lowercaseTrim, + chunkArray, + parseEmojis, }; diff --git a/src/fetchers/repo-fetcher.js b/src/fetchers/repo-fetcher.js index 7e441084..0bad5f2b 100644 --- a/src/fetchers/repo-fetcher.js +++ b/src/fetchers/repo-fetcher.js @@ -1,6 +1,11 @@ +// @ts-check const retryer = require("../common/retryer"); -const { request } = require("../common/utils"); +const { request, MissingParamError } = require("../common/utils"); +/** + * @param {import('Axios').AxiosRequestHeaders} variables + * @param {string} token + */ const fetcher = (variables, token) => { return request( { @@ -38,15 +43,24 @@ const fetcher = (variables, token) => { variables, }, { - Authorization: `bearer ${token}`, + Authorization: `token ${token}`, }, ); }; +const urlExample = "/api/pin?username=USERNAME&repo=REPO_NAME"; + +/** + * @param {string} username + * @param {string} reponame + * @returns {Promise} + */ async function fetchRepo(username, reponame) { - if (!username || !reponame) { - throw new Error("Invalid username or reponame"); + if (!username && !reponame) { + throw new MissingParamError(["username", "repo"], urlExample); } + if (!username) throw new MissingParamError(["username"], urlExample); + if (!reponame) throw new MissingParamError(["repo"], urlExample); let res = await retryer(fetcher, { login: username, repo: reponame }); @@ -63,7 +77,10 @@ async function fetchRepo(username, reponame) { if (!data.user.repository || data.user.repository.isPrivate) { throw new Error("User Repository Not found"); } - return data.user.repository; + return { + ...data.user.repository, + starCount: data.user.repository.stargazers.totalCount, + }; } if (isOrg) { @@ -73,7 +90,10 @@ async function fetchRepo(username, reponame) { ) { throw new Error("Organization Repository Not found"); } - return data.organization.repository; + return { + ...data.organization.repository, + starCount: data.organization.repository.stargazers.totalCount, + }; } } diff --git a/src/fetchers/stats-fetcher.js b/src/fetchers/stats-fetcher.js index ce5a4af7..dc9be5bb 100644 --- a/src/fetchers/stats-fetcher.js +++ b/src/fetchers/stats-fetcher.js @@ -1,12 +1,22 @@ -const axios = require("axios"); +// @ts-check +const axios = require("axios").default; const githubUsernameRegex = require("github-username-regex"); const retryer = require("../common/retryer"); const calculateRank = require("../calculateRank"); -const { request, logger, CustomError } = require("../common/utils"); +const { + request, + logger, + CustomError, + MissingParamError, +} = require("../common/utils"); require("dotenv").config(); +/** + * @param {import('axios').AxiosRequestHeaders} variables + * @param {string} token + */ const fetcher = (variables, token) => { return request( { @@ -25,7 +35,10 @@ const fetcher = (variables, token) => { pullRequests(first: 1) { totalCount } - issues(first: 1) { + openIssues: issues(states: OPEN) { + totalCount + } + closedIssues: issues(states: CLOSED) { totalCount } followers { @@ -34,6 +47,7 @@ const fetcher = (variables, token) => { repositories(first: 100, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}) { totalCount nodes { + name stargazers { totalCount } @@ -66,30 +80,38 @@ const totalCommitsFetcher = async (username) => { headers: { "Content-Type": "application/json", Accept: "application/vnd.github.cloak-preview", - Authorization: `bearer ${token}`, + Authorization: `token ${token}`, }, }); }; try { let res = await retryer(fetchTotalCommits, { login: username }); - if (res.data.total_count) { + let total_count = res.data.total_count; + if (!!total_count && !isNaN(total_count)) { return res.data.total_count; } } catch (err) { logger.log(err); - // just return 0 if there is something wrong so that - // we don't break the whole app - return 0; } + // just return 0 if there is something wrong so that + // we don't break the whole app + return 0; }; +/** + * @param {string} username + * @param {boolean} count_private + * @param {boolean} include_all_commits + * @returns {Promise} + */ async function fetchStats( username, count_private = false, include_all_commits = false, + exclude_repo = [], ) { - if (!username) throw Error("Invalid username"); + if (!username) throw new MissingParamError(["username"]); const stats = { name: "", @@ -113,8 +135,17 @@ async function fetchStats( const user = res.data.data.user; + // populate repoToHide map for quick lookup + // while filtering out + let repoToHide = {}; + if (exclude_repo) { + exclude_repo.forEach((repoName) => { + repoToHide[repoName] = true; + }); + } + stats.name = user.name || user.login; - stats.totalIssues = user.issues.totalCount; + stats.totalIssues = user.openIssues.totalCount + user.closedIssues.totalCount; // normal commits stats.totalCommits = user.contributionsCollection.totalCommitContributions; @@ -134,9 +165,14 @@ async function fetchStats( stats.totalPRs = user.pullRequests.totalCount; stats.contributedTo = user.repositoriesContributedTo.totalCount; - stats.totalStars = user.repositories.nodes.reduce((prev, curr) => { - return prev + curr.stargazers.totalCount; - }, 0); + // Retrieve stars while filtering out repositories to be hidden + stats.totalStars = user.repositories.nodes + .filter((data) => { + return !repoToHide[data.name]; + }) + .reduce((prev, curr) => { + return prev + curr.stargazers.totalCount; + }, 0); stats.rank = calculateRank({ totalCommits: stats.totalCommits, diff --git a/src/fetchers/top-languages-fetcher.js b/src/fetchers/top-languages-fetcher.js index 7e248fb1..7c4c8785 100644 --- a/src/fetchers/top-languages-fetcher.js +++ b/src/fetchers/top-languages-fetcher.js @@ -1,7 +1,12 @@ -const { request, logger } = require("../common/utils"); +// @ts-check +const { request, logger, MissingParamError } = require("../common/utils"); const retryer = require("../common/retryer"); require("dotenv").config(); +/** + * @param {import('Axios').AxiosRequestHeaders} variables + * @param {string} token + */ const fetcher = (variables, token) => { return request( { @@ -29,13 +34,18 @@ const fetcher = (variables, token) => { variables, }, { - Authorization: `bearer ${token}`, + Authorization: `token ${token}`, }, ); }; +/** + * @param {string} username + * @param {string[]} exclude_repo + * @returns {Promise} + */ async function fetchTopLanguages(username, exclude_repo = []) { - if (!username) throw Error("Invalid username"); + if (!username) throw new MissingParamError(["username"]); const res = await retryer(fetcher, { login: username }); @@ -58,14 +68,10 @@ async function fetchTopLanguages(username, exclude_repo = []) { // filter out repositories to be hidden repoNodes = repoNodes .sort((a, b) => b.size - a.size) - .filter((name) => { - return !repoToHide[name.name]; - }); + .filter((name) => !repoToHide[name.name]); repoNodes = repoNodes - .filter((node) => { - return node.languages.edges.length > 0; - }) + .filter((node) => node.languages.edges.length > 0) // flatten the list of language nodes .reduce((acc, curr) => curr.languages.edges.concat(acc), []) .reduce((acc, prev) => { diff --git a/src/fetchers/types.d.ts b/src/fetchers/types.d.ts new file mode 100644 index 00000000..854e1315 --- /dev/null +++ b/src/fetchers/types.d.ts @@ -0,0 +1,104 @@ +export type RepositoryData = { + name: string; + nameWithOwner: string; + isPrivate: boolean; + isArchived: boolean; + isTemplate: boolean; + stargazers: { totalCount: number }; + description: string; + primaryLanguage: { + color: string; + id: string; + name: string; + }; + forkCount: number; + starCount: number; +}; + +export type StatsData = { + name: string; + totalPRs: number; + totalCommits: number; + totalIssues: number; + totalStars: number; + contributedTo: number; + rank: { level: string; score: number }; +}; + +export type Lang = { + name: string; + color: string; + size: number; +}; + +export type TopLangData = Record; + +export type WakaTimeData = { + categories: { + digital: string; + hours: number; + minutes: number; + name: string; + percent: number; + text: string; + total_seconds: number; + }[]; + daily_average: number; + daily_average_including_other_language: number; + days_including_holidays: number; + days_minus_holidays: number; + editors: { + digital: string; + hours: number; + minutes: number; + name: string; + percent: number; + text: string; + total_seconds: number; + }[]; + holidays: number; + human_readable_daily_average: string; + human_readable_daily_average_including_other_language: string; + human_readable_total: string; + human_readable_total_including_other_language: string; + id: string; + is_already_updating: boolean; + is_coding_activity_visible: boolean; + is_including_today: boolean; + is_other_usage_visible: boolean; + is_stuck: boolean; + is_up_to_date: boolean; + languages: { + digital: string; + hours: number; + minutes: number; + name: string; + percent: number; + text: string; + total_seconds: number; + }[]; + operating_systems: { + digital: string; + hours: number; + minutes: number; + name: string; + percent: number; + text: string; + total_seconds: number; + }[]; + percent_calculated: number; + range: string; + status: string; + timeout: number; + total_seconds: number; + total_seconds_including_other_language: number; + user_id: string; + username: string; + writes_only: boolean; +}; + +export type WakaTimeLang = { + name: string; + text: string; + percent: number; +}; diff --git a/src/fetchers/wakatime-fetcher.js b/src/fetchers/wakatime-fetcher.js index 55809e2a..fe06c638 100644 --- a/src/fetchers/wakatime-fetcher.js +++ b/src/fetchers/wakatime-fetcher.js @@ -1,11 +1,18 @@ const axios = require("axios"); +const { MissingParamError } = require("../common/utils"); +/** + * @param {{username: string, api_domain: string, range: string}} props + * @returns {Promise} + */ const fetchWakatimeStats = async ({ username, api_domain, range }) => { + if (!username) throw new MissingParamError(["username"]); + try { const { data } = await axios.get( `https://${ - api_domain ? api_domain.replace(/[^a-z-.0-9]/gi, "") : "wakatime.com" - }/api/v1/users/${username}/stats/${range || ''}?is_including_today=true`, + api_domain ? api_domain.replace(/\/$/gi, "") : "wakatime.com" + }/api/v1/users/${username}/stats/${range || ""}?is_including_today=true`, ); return data.data; diff --git a/src/getStyles.js b/src/getStyles.js index b15f46d0..e76b51f4 100644 --- a/src/getStyles.js +++ b/src/getStyles.js @@ -1,14 +1,22 @@ +// @ts-check +/** + * @param {number} value + */ const calculateCircleProgress = (value) => { - let radius = 40; - let c = Math.PI * (radius * 2); + const radius = 40; + const c = Math.PI * (radius * 2); if (value < 0) value = 0; if (value > 100) value = 100; - let percentage = ((100 - value) / 100) * c; - return percentage; + return ((100 - value) / 100) * c; }; +/** + * + * @param {{progress: number}} param0 + * @returns + */ const getProgressAnimation = ({ progress }) => { return ` @keyframes rankAnimation { @@ -44,6 +52,15 @@ const getAnimations = () => { `; }; +/** + * @param {{ + * titleColor?: string | string[] + * textColor?: string | string[] + * iconColor?: string | string[] + * show_icons?: boolean; + * progress?: number; + * }} args + */ const getStyles = ({ titleColor, textColor, @@ -55,6 +72,10 @@ const getStyles = ({ .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; @@ -64,6 +85,7 @@ const getStyles = ({ animation: scaleInAnimation 0.3s ease-in-out forwards; } + .not_bold { font-weight: 400 } .bold { font-weight: 700 } .icon { fill: ${iconColor}; diff --git a/src/translations.js b/src/translations.js index 7f3a0f1f..31b2d218 100644 --- a/src/translations.js +++ b/src/translations.js @@ -6,6 +6,7 @@ const statCardLocales = ({ name, apostrophe }) => { "statcard.title": { ar: `${encodedName} إحصائيات غيت هاب`, cn: `${encodedName} 的 GitHub 统计数据`, + "zh-tw": `${encodedName} 的 GitHub 統計數據`, cs: `GitHub statistiky uživatele ${encodedName}`, de: `${encodedName + apostrophe} GitHub-Statistiken`, en: `${encodedName}'${apostrophe} GitHub Stats`, @@ -16,7 +17,7 @@ const statCardLocales = ({ name, apostrophe }) => { it: `Statistiche GitHub di ${encodedName}`, ja: `${encodedName}の GitHub 統計`, kr: `${encodedName}의 GitHub 통계`, - nl: `${encodedName}'${apostrophe} GitHub Statistieken`, + nl: `${encodedName}'${apostrophe} GitHub-statistieken`, "pt-pt": `Estatísticas do GitHub de ${encodedName}`, "pt-br": `Estatísticas do GitHub de ${encodedName}`, np: `${encodedName}'${apostrophe} गिटहब तथ्याङ्क`, @@ -24,25 +25,30 @@ const statCardLocales = ({ name, apostrophe }) => { ru: `Статистика GitHub пользователя ${encodedName}`, "uk-ua": `Статистика GitHub користувача ${encodedName}`, id: `Statistik GitHub ${encodedName}`, + ml: `${encodedName}'${apostrophe} ഗിറ്റ്ഹബ് സ്ഥിതിവിവരക്കണക്കുകൾ`, my: `Statistik GitHub ${encodedName}`, sk: `GitHub štatistiky používateľa ${encodedName}`, tr: `${encodedName} Hesabının GitHub Yıldızları`, pl: `Statystyki GitHub użytkownika ${encodedName}`, + uz: `${encodedName}ning Github'dagi statistikasi`, + vi: `Thống Kê GitHub ${encodedName}`, + se: `GitHubstatistik för ${encodedName}`, }, "statcard.totalstars": { ar: "مجموع النجوم", cn: "获标星数(star)", + "zh-tw": "獲標星數(star)", cs: "Celkem hvězd", - de: "Sterne Insgesamt", - en: "Total Stars", - bn: "সর্বমোট Stars", + de: "Insgesamt erhaltene Sterne", + en: "Total Stars Earned", + bn: "সর্বমোট Star", es: "Estrellas totales", fr: "Total d'étoiles", hu: "Csillagok", it: "Stelle totali", ja: "スターされた数", kr: "받은 스타 수", - nl: "Totale Sterren", + nl: "Totaal Sterren Ontvangen", "pt-pt": "Total de estrelas", "pt-br": "Total de estrelas", np: "कुल ताराहरू", @@ -50,25 +56,30 @@ const statCardLocales = ({ name, apostrophe }) => { ru: "Всего звезд", "uk-ua": "Всього зірок", id: "Total Bintang", + ml: "ആകെ നക്ഷത്രങ്ങൾ", my: "Jumlah Bintang", sk: "Hviezdy", tr: "Toplam Yıldız", - pl: "Liczba Gwiazdek", + pl: "Liczba Gwiazdek dostanych", + uz: "Yulduzchalar", + vi: "Tổng Số Sao", + se: "Antal intjänade stjärnor", }, "statcard.commits": { ar: "مجموع الحفظ", cn: "累计提交数(commit)", + "zh-tw": "累計提交數(commit)", cs: "Celkem commitů", de: "Anzahl Commits", en: "Total Commits", - bn: "সর্বমোট Commits", + bn: "সর্বমোট Commit", es: "Commits totales", - fr: "Total des validations", + fr: "Total des Commits", hu: "Összes commit", it: "Commit totali", ja: "合計コミット数", kr: "전체 커밋 수", - nl: "Totale Commits", + nl: "Aantal commits", "pt-pt": "Total de Commits", "pt-br": "Total de Commits", np: "कुल Commits", @@ -76,25 +87,30 @@ const statCardLocales = ({ name, apostrophe }) => { ru: "Всего коммитов", "uk-ua": "Всього коммітов", id: "Total Komitmen", + ml: "ആകെ കമ്മിറ്റുകൾ", my: "Jumlah Komitmen", sk: "Všetky commity", tr: "Toplam Commit", pl: "Wszystkie commity", + uz: "'Commit'lar", + vi: "Tổng Số Cam Kết", + se: "Totalt antal commits", }, "statcard.prs": { ar: "مجموع طلبات السحب", cn: "拉取请求数(PR)", + "zh-tw": "拉取請求數(PR)", cs: "Celkem PRs", de: "PRs Insgesamt", en: "Total PRs", - bn: "সর্বমোট PRs", + bn: "সর্বমোট PR", es: "PRs totales", - fr: "Total des PR", + fr: "Total des PRs", hu: "Összes PR", it: "PR totali", ja: "合計 PR", kr: "PR 횟수", - nl: "Totale PR's", + nl: "Aantal PR's", "pt-pt": "Total de PRs", "pt-br": "Total de PRs", np: "कुल PRs", @@ -102,25 +118,30 @@ const statCardLocales = ({ name, apostrophe }) => { ru: "Всего pull request`ов", "uk-ua": "Всього pull request`iв", id: "Total Permintaan Tarik", + ml: "ആകെ പുൾ അഭ്യർത്ഥനകൾ", my: "Jumlah PR", sk: "Všetky PR", tr: "Toplam PR", - pl: "Wszystkie PR", + pl: "Wszystkie PR-y", + uz: "'Pull Request'lar", + vi: "Tổng Số PR", + se: "Totalt antal PR", }, "statcard.issues": { ar: "مجموع التحسينات", cn: "指出问题数(issue)", + "zh-tw": "指出問題數(issue)", cs: "Celkem problémů", de: "Anzahl Issues", en: "Total Issues", - bn: "সর্বমোট Issues", + bn: "সর্বমোট Issue", es: "Issues totales", fr: "Nombre total d'incidents", hu: "Összes hibajegy", it: "Segnalazioni totali", ja: "合計 issue", kr: "이슈 개수", - nl: "Totale Issues", + nl: "Aantal kwesties", "pt-pt": "Total de Issues", "pt-br": "Total de Issues", np: "कुल मुद्दाहरू", @@ -128,14 +149,19 @@ const statCardLocales = ({ name, apostrophe }) => { ru: "Всего issue", "uk-ua": "Всього issue", id: "Total Masalah Dilaporkan", + ml: "ആകെ ലക്കങ്ങൾ", my: "Jumlah Isu Dilaporkan", sk: "Všetky problémy", tr: "Toplam Hata", - pl: "Wszystkie problemy", + pl: "Wszystkie Issues", + uz: "'Issue'lar", + vi: "Tổng Số Vấn Đề", + se: "Total antal issues", }, "statcard.contribs": { ar: "ساهم في", cn: "参与项目数", + "zh-tw": "參與項目數", cs: "Přispěl k", de: "Beigetragen zu", en: "Contributed to", @@ -144,7 +170,7 @@ const statCardLocales = ({ name, apostrophe }) => { fr: "Contribué à", hu: "Hozzájárulások", it: "Ha contribuito a", - ja: "コントリビュートしたリポジトリ", + ja: "貢献したリポジトリ", kr: "전체 기여도", nl: "Bijgedragen aan", "pt-pt": "Contribuiu em", @@ -154,10 +180,14 @@ const statCardLocales = ({ name, apostrophe }) => { ru: "Внёс вклад в", "uk-ua": "Вніс внесок у", id: "Berkontribusi ke", + ml: "സമർപ്പിച്ചിരിക്കുന്നത്", my: "Menyumbang kepada", sk: "Účasti", tr: "Katkı Verildi", - pl: "Udziały", + pl: "Kontrybucje", + uz: "Hissa qoʻshgan", + vi: "Đã Đóng Góp", + se: "Bidragit till", }, }; }; @@ -165,11 +195,13 @@ const statCardLocales = ({ name, apostrophe }) => { const repoCardLocales = { "repocard.template": { ar: "قالب", + bn: "টেমপ্লেট", cn: "模板", + "zh-tw": "模板", cs: "Šablona", de: "Vorlage", en: "Template", - es: "Planitlla", + es: "Plantilla", fr: "Modèle", hu: "Sablon", it: "Template", @@ -183,14 +215,19 @@ const repoCardLocales = { ru: "Шаблон", "uk-ua": "Шаблон", id: "Pola", + ml: "ടെംപ്ലേറ്റ്", my: "Templat", sk: "Šablóna", tr: "Şablon", pl: "Szablony", + vi: "Mẫu", + se: "Mall", }, "repocard.archived": { ar: "محفوظ", + bn: "আর্কাইভড", cn: "已归档", + "zh-tw": "已歸檔", cs: "Archivováno", de: "Archiviert", en: "Archived", @@ -208,10 +245,13 @@ const repoCardLocales = { ru: "Архивирован", "uk-ua": "Архивирован", id: "Arsip", + ml: "ശേഖരിച്ചത്", my: "Arkib", sk: "Archivované", tr: "Arşiv", pl: "Zarchiwizowano", + vi: "Đã Lưu Trữ", + se: "Arkiverade", }, }; @@ -219,8 +259,10 @@ const langCardLocales = { "langcard.title": { ar: "أكثر اللغات إستخداماً", cn: "最常用的语言", + "zh-tw": "最常用的語言", cs: "Nejpoužívanější jazyky", de: "Meist verwendete Sprachen", + bn: "সর্বাধিক ব্যবহৃত ভাষা সমূহ", en: "Most Used Languages", es: "Lenguajes más usados", fr: "Langages les plus utilisés", @@ -236,10 +278,13 @@ const langCardLocales = { ru: "Наиболее часто используемые языки", "uk-ua": "Найбільш часто використовувані мови", id: "Bahasa Yang Paling Banyak Digunakan", + ml: "കൂടുതൽ ഉപയോഗിച്ച ഭാഷകൾ", my: "Bahasa Paling Digunakan", sk: "Najviac používané jazyky", tr: "En Çok Kullanılan Diller", pl: "Najczęściej używane języki", + vi: "Ngôn Ngữ Thường Sử Dụng", + se: "Mest använda språken", }, }; @@ -247,16 +292,18 @@ const wakatimeCardLocales = { "wakatimecard.title": { ar: "إحصائيات واكا تايم", cn: "Wakatime 周统计", + "zh-tw": "Wakatime 周統計", cs: "Statistiky Wakatime", de: "Wakatime Status", en: "Wakatime Stats", + bn: "Wakatime স্ট্যাটাস", es: "Estadísticas de Wakatime", fr: "Statistiques de Wakatime", hu: "Wakatime statisztika", it: "Statistiche Wakatime", ja: "Wakatime ワカタイム統計", kr: "Wakatime 주간 통계", - nl: "Takwimu za Wakatime", + nl: "Wakatime-statistieken", "pt-pt": "Estatísticas Wakatime", "pt-br": "Estatísticas Wakatime", np: "Wakatime तथ्या .्क", @@ -264,24 +311,29 @@ const wakatimeCardLocales = { ru: "Статистика Wakatime", "uk-ua": "Статистика Wakatime", id: "Status Wakatime", + ml: "വേക്ക് ടൈം സ്ഥിതിവിവരക്കണക്കുകൾ", my: "Statistik Wakatime", sk: "Wakatime štatistika", tr: "Waketime İstatistikler", pl: "statystyki Wakatime", + vi: "Thống Kê Wakatime", + se: "Wakatime statistik", }, "wakatimecard.nocodingactivity": { ar: "لا يوجد نشاط برمجي لهذا الأسبوع", cn: "本周没有编程活动", + "zh-tw": "本周沒有編程活動", cs: "Tento týden žádná aktivita v kódování", de: "Keine Aktivitäten in dieser Woche", en: "No coding activity this week", + bn: "এই সপ্তাহে কোন কোডিং অ্যাক্টিভিটি নেই", es: "No hay actividad de codificación esta semana", fr: "Aucune activité de codage cette semaine", hu: "Nem volt aktivitás ezen a héten", it: "Nessuna attività in questa settimana", ja: "今週のコーディング活動はありません", kr: "이번 주 작업내역 없음", - nl: "Geen coderings activiet deze week", + nl: "Geen programmeeractiviteit deze week", "pt-pt": "Sem atividade esta semana", "pt-br": "Nenhuma atividade de codificação esta semana", np: "यस हप्ता कुनै कोडिंग गतिविधि छैन", @@ -289,10 +341,14 @@ const wakatimeCardLocales = { ru: "На этой неделе не было активности", "uk-ua": "На цьому тижні не було активності", id: "Tidak ada aktivitas perkodingan minggu ini", + ml: "ഈ ആഴ്ച കോഡിംഗ് പ്രവർത്തനങ്ങളൊന്നുമില്ല", my: "Tiada aktiviti pengekodan minggu ini", sk: "Žiadna kódovacia aktivita tento týždeň", tr: "Bu hafta herhangi bir kod yazma aktivitesi olmadı", pl: "Brak aktywności w tym tygodniu", + uz: "Bu hafta faol bo'lmadi", + vi: "Không Có Hoạt Động Trong Tuần Này", + se: "Ingen aktivitet denna vecka", }, }; diff --git a/tests/__snapshots__/renderWakatimeCard.test.js.snap b/tests/__snapshots__/renderWakatimeCard.test.js.snap index c46f950c..bd625802 100644 --- a/tests/__snapshots__/renderWakatimeCard.test.js.snap +++ b/tests/__snapshots__/renderWakatimeCard.test.js.snap @@ -8,27 +8,40 @@ exports[`Test Render Wakatime Card should render correctly 1`] = ` viewBox=\\"0 0 495 150\\" fill=\\"none\\" xmlns=\\"http://www.w3.org/2000/svg\\" + role=\\"img\\" + aria-labelledby=\\"descId\\" > + +