diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 198bc80ef0fe46..367ae4f302d8b0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -15,7 +15,7 @@ body: required: true - type: textarea attributes: - label: Expected behaviour + label: Expected behavior description: A clear and concise description of what you expected to happen. - type: textarea diff --git a/.github/labeler.yml b/.github/labeler.yml index be97765f07e42b..fad3eeeb8d1014 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,3 +1,4 @@ themes: themes/index.js doc-translation: docs/* card-i18n: src/translations.js +documentation: readme.md diff --git a/.github/workflows/generate-theme-doc.yml b/.github/workflows/generate-theme-doc.yml index d5fac06381943b..75f6511f09015d 100644 --- a/.github/workflows/generate-theme-doc.yml +++ b/.github/workflows/generate-theme-doc.yml @@ -23,6 +23,10 @@ jobs: node-version: ${{ matrix.node-version }} cache: npm + # Fix the unsafe repo error which was introduced by the CVE-2022-24765 git patches. + - name: Fix unsafe repo error + run: git config --global --add safe.directory ${{ github.workspace }} + - name: npm install, generate readme run: | npm ci diff --git a/.github/workflows/prs-cache-clean.yml b/.github/workflows/prs-cache-clean.yml new file mode 100644 index 00000000000000..8ee4670e015340 --- /dev/null +++ b/.github/workflows/prs-cache-clean.yml @@ -0,0 +1,33 @@ +name: prs cache clean +on: + pull_request: + types: + - closed + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Cleanup + run: | + gh extension install actions/gh-actions-cache + + REPO=${{ github.repository }} + BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge" + + echo "Fetching list of cache key" + cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 ) + + ## Setting this to not fail the workflow while deleting cache keys. + set +e + echo "Deleting caches..." + for cacheKey in $cacheKeysForPR + do + gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm + done + echo "Done" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fe34668d3e8d27..e8fc84a7df963a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,4 +34,4 @@ jobs: npm run format:check - name: Code Coverage - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 diff --git a/.github/workflows/update-langs.yaml b/.github/workflows/update-langs.yaml new file mode 100644 index 00000000000000..ad6bfb6213b8f7 --- /dev/null +++ b/.github/workflows/update-langs.yaml @@ -0,0 +1,44 @@ +name: Update supported languages +on: + schedule: + - cron: "0 0 */30 * *" + +jobs: + updateLanguages: + if: github.repository == 'anuraghazra/github-readme-stats' + name: Update supported languages + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [16.x] + + steps: + - uses: actions/checkout@v3 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: npm + + - name: Install dependencies + run: npm ci + env: + CI: true + + - name: Run update-languages-json.js script + run: npm run generate-langs-json + + - name: Create Pull Request if upstream language file is changed + uses: peter-evans/create-pull-request@v4 + with: + commit-message: "refactor: update languages JSON" + branch: "update_langs/patch" + delete-branch: true + title: Update languages JSON + body: + "The + [update-langs](https://github.com/anuraghazra/github-readme-stats/actions/workflows/update-langs.yaml) + action found new/updated languages in the [upstream languages JSON + file](https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml)." + labels: "ci, lang-card" diff --git a/.gitignore b/.gitignore index 2cdfa3d3348087..25017502d486aa 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ vercel_token # IDE .vscode *.code-workspace + +.vercel diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0d4b558abe6f10..7d450d6076d8e7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,11 +2,11 @@ We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: -- Reporting an issue -- Discussing the current state of the code -- Submitting a fix -- Proposing new features -- Becoming a maintainer +- Reporting [an issue](https://github.com/anuraghazra/github-readme-stats/issues/new?assignees=&labels=bug&template=bug_report.yml). +- [Discussing](https://github.com/anuraghazra/github-readme-stats/discussions) the current state of the code. +- Submitting [a fix](https://github.com/anuraghazra/github-readme-stats/compare). +- Proposing [new features](https://github.com/anuraghazra/github-readme-stats/issues/new?assignees=&labels=enhancement&template=feature_request.yml). +- Becoming a maintainer. ## All Changes Happen Through Pull Requests @@ -33,11 +33,15 @@ _(make sure you already have a [Vercel](https://vercel.com/) account)_ 1. Install [Vercel CLI](https://vercel.com/download). 2. Fork the repository and clone the code to your local machine. 3. Run `npm install` in the repository root. -4. Run the command "vercel" in the root and follow the steps there. +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 . +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 . +9. The cards will then be available from this local endpoint (i.e. `https://localhost:3000/api?username=anuraghazra`). + +> **Note** +> You can also debug any tests using the [VSCode Jest extension](https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest). For more information see https://github.com/jest-community/vscode-jest/issues/912. ## Themes Contribution diff --git a/api/index.js b/api/index.js index b449d43b490801..29ff87f9af8639 100644 --- a/api/index.js +++ b/api/index.js @@ -35,7 +35,9 @@ export default async (req, res) => { locale, disable_animations, border_radius, + number_format, border_color, + rank_icon, } = req.query; res.setHeader("Content-Type", "image/svg+xml"); @@ -88,8 +90,10 @@ export default async (req, res) => { custom_title, border_radius, border_color, + number_format, locale: locale ? locale.toLowerCase() : null, disable_animations: parseBoolean(disable_animations), + rank_icon, }), ); } catch (err) { diff --git a/api/status/pat-info.js b/api/status/pat-info.js new file mode 100644 index 00000000000000..69d869ea2553e6 --- /dev/null +++ b/api/status/pat-info.js @@ -0,0 +1,139 @@ +/** + * @file Contains a simple cloud function that can be used to check which PATs are no + * longer working. It returns a list of valid PATs, expired PATs and PATs with errors. + * + * @description This function is currently rate limited to 1 request per 5 minutes. + */ + +import { logger, request, dateDiff } from "../../src/common/utils.js"; +export const RATE_LIMIT_SECONDS = 60 * 5; // 1 request per 5 minutes + +/** + * Simple uptime check fetcher for the PATs. + * + * @param {import('axios').AxiosRequestHeaders} variables + * @param {string} token + */ +const uptimeFetcher = (variables, token) => { + return request( + { + query: ` + query { + rateLimit { + remaining + resetAt + }, + }`, + variables, + }, + { + Authorization: `bearer ${token}`, + }, + ); +}; + +const getAllPATs = () => { + return Object.keys(process.env).filter((key) => /PAT_\d*$/.exec(key)); +}; + +/** + * Check whether any of the PATs is expired. + */ +const getPATInfo = async (fetcher, variables) => { + const details = {}; + const PATs = getAllPATs(); + + for (const pat of PATs) { + try { + const response = await fetcher(variables, process.env[pat]); + const errors = response.data.errors; + const hasErrors = Boolean(errors); + const errorType = errors?.[0]?.type; + const isRateLimited = + (hasErrors && errorType === "RATE_LIMITED") || + response.data.data?.rateLimit?.remaining === 0; + + // Store PATs with errors. + if (hasErrors && errorType !== "RATE_LIMITED") { + details[pat] = { + status: "error", + error: { + type: errors[0].type, + message: errors[0].message, + }, + }; + continue; + } else if (isRateLimited) { + const date1 = new Date(); + const date2 = new Date(response.data?.data?.rateLimit?.resetAt); + details[pat] = { + status: "exhausted", + remaining: 0, + resetIn: dateDiff(date2, date1) + " minutes", + }; + } else { + details[pat] = { + status: "valid", + remaining: response.data.data.rateLimit.remaining, + }; + } + } catch (err) { + // Store the PAT if it is expired. + const errorMessage = err.response?.data?.message?.toLowerCase(); + if (errorMessage === "bad credentials") { + details[pat] = { + status: "expired", + }; + } else if (errorMessage === "sorry. your account was suspended.") { + details[pat] = { + status: "suspended", + }; + } else { + throw err; + } + } + } + + const filterPATsByStatus = (status) => { + return Object.keys(details).filter((pat) => details[pat].status === status); + }; + + const sortedDetails = Object.keys(details) + .sort() + .reduce((obj, key) => { + obj[key] = details[key]; + return obj; + }, {}); + + return { + validPATs: filterPATsByStatus("valid"), + expiredPATs: filterPATsByStatus("expired"), + exhaustedPATs: filterPATsByStatus("exhausted"), + suspendedPATs: filterPATsByStatus("suspended"), + errorPATs: filterPATsByStatus("error"), + details: sortedDetails, + }; +}; + +/** + * Cloud function that returns information about the used PATs. + */ +export default async (_, res) => { + res.setHeader("Content-Type", "application/json"); + try { + // Add header to prevent abuse. + const PATsInfo = await getPATInfo(uptimeFetcher, {}); + if (PATsInfo) { + res.setHeader( + "Cache-Control", + `max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`, + ); + } + res.send(JSON.stringify(PATsInfo, null, 2)); + } catch (err) { + // Throw error if something went wrong. + logger.error(err); + res.setHeader("Cache-Control", "no-store"); + res.send("Something went wrong: " + err.message); + } +}; diff --git a/api/status/up.js b/api/status/up.js new file mode 100644 index 00000000000000..678a20b0b5c147 --- /dev/null +++ b/api/status/up.js @@ -0,0 +1,103 @@ +/** + * @file Contains a simple cloud function that can be used to check if the PATs are still + * functional. + * + * @description This function is currently rate limited to 1 request per 5 minutes. + */ + +import retryer from "../../src/common/retryer.js"; +import { logger, request } from "../../src/common/utils.js"; + +export const RATE_LIMIT_SECONDS = 60 * 5; // 1 request per 5 minutes + +/** + * Simple uptime check fetcher for the PATs. + * + * @param {import('axios').AxiosRequestHeaders} variables + * @param {string} token + */ +const uptimeFetcher = (variables, token) => { + return request( + { + query: ` + query { + rateLimit { + remaining + } + } + `, + variables, + }, + { + Authorization: `bearer ${token}`, + }, + ); +}; + +/** + * Creates Json response that can be used for shields.io dynamic card generation. + * + * @param {*} up Whether the PATs are up or not. + * @returns Dynamic shields.io JSON response object. + * + * @see https://shields.io/endpoint. + */ +const shieldsUptimeBadge = (up) => { + const schemaVersion = 1; + const isError = true; + const label = "Public Instance"; + const message = up ? "up" : "down"; + const color = up ? "brightgreen" : "red"; + return { + schemaVersion, + label, + message, + color, + isError, + }; +}; + +/** + * Cloud function that returns whether the PATs are still functional. + */ +export default async (req, res) => { + let { type } = req.query; + type = type ? type.toLowerCase() : "boolean"; + + res.setHeader("Content-Type", "application/json"); + + try { + let PATsValid = true; + try { + await retryer(uptimeFetcher, {}); + } catch (err) { + PATsValid = false; + } + + if (PATsValid) { + res.setHeader( + "Cache-Control", + `max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`, + ); + } else { + res.setHeader("Cache-Control", "no-store"); + } + + switch (type) { + case "shields": + res.send(shieldsUptimeBadge(PATsValid)); + break; + case "json": + res.send({ up: PATsValid }); + break; + default: + res.send(PATsValid); + break; + } + } catch (err) { + // Return fail boolean if something went wrong. + logger.error(err); + res.setHeader("Cache-Control", "no-store"); + res.send("Something went wrong: " + err.message); + } +}; diff --git a/api/top-langs.js b/api/top-langs.js index 19cccb894e33a9..cde0a9af08a93b 100644 --- a/api/top-langs.js +++ b/api/top-langs.js @@ -25,11 +25,14 @@ export default async (req, res) => { layout, langs_count, exclude_repo, + size_weight, + count_weight, custom_title, locale, border_radius, border_color, disable_animations, + hide_progress, } = req.query; res.setHeader("Content-Type", "image/svg+xml"); @@ -45,6 +48,8 @@ export default async (req, res) => { const topLangs = await fetchTopLanguages( username, parseArray(exclude_repo), + size_weight, + count_weight, ); const cacheSeconds = clampValue( @@ -77,6 +82,7 @@ export default async (req, res) => { border_color, locale: locale ? locale.toLowerCase() : null, disable_animations: parseBoolean(disable_animations), + hide_progress: parseBoolean(hide_progress), }), ); } catch (err) { diff --git a/api/wakatime.js b/api/wakatime.js index d439c5b7ac8c67..7ae93b57993212 100644 --- a/api/wakatime.js +++ b/api/wakatime.js @@ -28,7 +28,6 @@ export default async (req, res) => { langs_count, hide, api_domain, - range, border_radius, border_color, } = req.query; @@ -40,7 +39,7 @@ export default async (req, res) => { } try { - const stats = await fetchWakatimeStats({ username, api_domain, range }); + const stats = await fetchWakatimeStats({ username, api_domain }); let cacheSeconds = clampValue( parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10), diff --git a/docs/readme_cn.md b/docs/readme_cn.md index 17fd2b710f2202..8fda35b348140a 100644 --- a/docs/readme_cn.md +++ b/docs/readme_cn.md @@ -53,6 +53,8 @@ Nederlands . नेपाली + . + Türkçe

喜欢这个项目?请考虑捐赠来帮助它完善! @@ -138,7 +140,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr - `bg_color` - 卡片背景颜色 _(十六进制色码)_ **或者** 以 _angle,start,end_ 的形式渐变 - `hide_border` - 隐藏卡的边框 _(布尔值)_ - `theme` - 主题名称,从[所有可用主题](../themes/README.md)中选择 -- `cache_seconds` - 手动设置缓存头 _(最小值: 1800,最大值: 86400)_ +- `cache_seconds` - 手动设置缓存头 _(最小值: 14400,最大值: 86400)_ - `locale` - 在卡片中设置语言 _(例如 cn, de, es, 等等)_ ##### bg_color 渐变 @@ -169,7 +171,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr - `hide` - 从卡片中隐藏指定语言 _(Comma seperated values)_ - `hide_title` - _(boolean)_ -- `layout` - 在两个可用布局 `default` & `compact` 间切换 +- `layout` - 提供五種佈局 `normal` & `compact` & `donut` & `donut-vertical` & `pie` 间切换 - `card_width` - 手动设置卡片的宽度 _(number)_ > :warning: **重要:** diff --git a/docs/readme_de.md b/docs/readme_de.md index 55523fe342ef00..bb3e312909f3a9 100644 --- a/docs/readme_de.md +++ b/docs/readme_de.md @@ -54,6 +54,8 @@ Nederlands . नेपाली + . + Türkçe

Du magst das Projekt? Wie wäre es mit einer kleinen Spende um es weiterhin am Leben zu erhalten? @@ -128,7 +130,7 @@ Du kannst das Erscheinungsbild deiner `Stats Card` oder `Repo Card`, mithilfe vo - `bg_color` - Hintergrundfarbe _(hex color)_ **oder** ein Farbverlauf in der Form von _winkel,start,ende_ - `hide_border` - Blendet den Rand der Karte aus _(Boolean)_ - `theme` - Name des Erscheinungsbildes/Themes [alle verfügbaren Themes](../themes/README.md) -- `cache_seconds` - manuelles festlegen der Cachezeiten _(min: 1800, max: 86400)_ +- `cache_seconds` - manuelles festlegen der Cachezeiten _(min: 14400, max: 86400)_ - `locale` - Stellen Sie die Sprache auf der Karte ein _(z.B. cn, de, es, etc.)_ ##### Farbverlauf in bg_color @@ -159,7 +161,7 @@ Du kannst mehrere, mit Kommas separierte, Werte in der bg_color Option angeben, - `hide` - Verbirgt die angegebenen Sprachen von der Karte _(Komma separierte Werte)_ - `hide_title` - _(Boolean)_ -- `layout` - Wechsel zwischen den zwei verfügbaren Layouts `default` & `compact` +- `layout` - Wechseln Sie zwischen den fünf verfügbaren Layouts `normal` & `compact` & `donut` & `donut-vertical` & `pie` - `card_width` - Lege die Breite der Karte manuell fest _(Zahl)_ > :warning: **Wichtig:** @@ -251,18 +253,18 @@ Du kannst die `&layout=compact` Option nutzen, um das Kartendesign zu ändern. Ä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) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) ``` ### Beispiel -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats) - Kompaktes Layout -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod&layout=compact)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok&layout=compact)](https://github.com/anuraghazra/github-readme-stats) --- diff --git a/docs/readme_es.md b/docs/readme_es.md index 5ddceec7b5e99f..5815e9089a90cb 100644 --- a/docs/readme_es.md +++ b/docs/readme_es.md @@ -142,7 +142,7 @@ Puedes personalizar el aspecto de tu `Tarjeta de Estadísticas` o `Tarjeta de Re - `bg_color` - Color de fondo _(hex color)_ - `hide_border` - Oculta el borde de la tarjeta _(booleano)_ - `theme` - Nombre del tema, elige uno de [todos los temas disponible ](../themes/README.md) -- `cache_seconds` - Cache _(min: 1800, max: 86400)_ +- `cache_seconds` - Cache _(min: 14400, max: 86400)_ - `locale` - configurar el idioma en la tarjeta _(p.ej. cn, de, es, etc.)_ ##### Gradiente en `bg_color` @@ -175,7 +175,7 @@ Puedes pasar mútliples valores separados por coma en la opción `bg_color` para - `hide` - Oculta de la tarjeta los lenguajes especificados _(valores separados por comas)_ - `hide_title` - _(booleano)_ -- `layout` - Cambia entre los dos diseños disponibles `default` & `compact` +- `layout` - Cambiar entre los cinco diseños disponibles `normal` & `compact` & `donut` & `donut-vertical` & `pie` - `card_width` - Establece el ancho de la tarjeta manualmente _(número)_ - `langs_count` - Muestra más lenguajes en la tarjeta, entre 1-10, por defecto 5 _(número)_ - `exclude_repo` - Excluye los repositorios especificados _(valores separados por comas)_ @@ -282,18 +282,18 @@ Puedes usar la opción `& layout = compact` para cambiar el diseño de la tarjet cambia el valor del parámetro `?username=` a tu username en [Wakatime](https://wakatime.com). ```md -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) ``` ### Ejemplo -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats) - Diseño compacto -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod&layout=compact)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok&layout=compact)](https://github.com/anuraghazra/github-readme-stats) --- @@ -343,7 +343,7 @@ Escoja cualquiera de los [temas por defecto](#themes) - Tarjeta de Wakatime -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) --- diff --git a/docs/readme_fr.md b/docs/readme_fr.md index 20996bd66dda8a..1a597dd19c4baa 100644 --- a/docs/readme_fr.md +++ b/docs/readme_fr.md @@ -53,6 +53,8 @@ Nederlands . नेपाली + . + Türkçe

Vous aimez ce projet? Pensez à faire un don pour l'améliorer! @@ -88,18 +90,6 @@ Pour masquer des statistiques spécifiques, vous pouvez passer un paramètre de ![Les Stats GitHub de Anurag](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=contribs,prs) ``` -### Ajouter le compte des contributions privées au compte des commits totaux - -Vous pouvez ajouter le compte de toutes vos contributions privées au compte total des engagements en utilisant le paramètre de requête `?count_private=true`. - -_Note: Si vous déployez vous-même ce projet, les contributions privées seront comptées par défaut ; sinon, vous devez choisir de partager les comptes de vos contributions privées._ - -> Options: `&count_private=true` - -```md -![Les Stats GitHub de Anurag](https://github-readme-stats.vercel.app/api?username=anuraghazra&count_private=true) -``` - ### Afficher les icônes Pour activer les icônes, vous pouvez passer `show_icons=true` dans le paramètre de requête, comme ceci : @@ -138,7 +128,7 @@ Vous pouvez personnaliser l'apparence de votre `Carte des stats` ou `Carte de d - `bg_color` - Couleur du fond de la carte _(hex color)_ **ou** un gradiant de la forme _angle,start,end_ - `hide_border` - Cache la bordure de la carte _(booléen)_ - `theme` - Nom du thème, parmis [tous les thèmes disponibles](../themes/README.md) -- `cache_seconds` - Paramétrer le cache manuellement _(min: 1800, max: 86400)_ +- `cache_seconds` - Paramétrer le cache manuellement _(min: 14400, max: 86400)_ - `locale` - définir la langue de la carte _(par exemple. cn, de, es, etc.)_ ##### Gradient in bg_color @@ -158,7 +148,7 @@ Vous pouvez fournir plusieurs valeurs (suivie d'une virgule) dans l'option bg_co - `hide_rank` - Masquer le rang _(boolean)_ - `show_icons` - Afficher les icônes _(boolean)_ - `include_all_commits` - Compter le total de commits au lieu de ne compter que les commits de l'année en cours _(boolean)_ -- `count_private` - Compter les commits privés _(boolean)_ +- `count_private` - Compter les contributions privées _(boolean)_ - `line_height` - Fixer la hauteur de la ligne entre les textes _(number)_ #### Repo Card Exclusive Options: @@ -169,7 +159,7 @@ Vous pouvez fournir plusieurs valeurs (suivie d'une virgule) dans l'option bg_co - `hide` - Masquer les langages spécifiés sur la carte _(Comma seperated values)_ - `hide_title` - Masquer le titre _(boolean)_ -- `layout` - Alterner entre 2 mise en page `default` & `compact` +- `layout` - Alterner entre 5 mise en page `normal` & `compact` & `donut` & `donut-vertical` & `pie` - `card_width` - Fixer la largeur de la carte manuellement _(number)_ > :warning: **Important:** diff --git a/docs/readme_it.md b/docs/readme_it.md index 1b2df96a5044d0..f1c20d1a48d16d 100644 --- a/docs/readme_it.md +++ b/docs/readme_it.md @@ -53,6 +53,8 @@ Nederlands . नेपाली + . + Türkçe

Se ti piace questo progetto, considera la possibilità di donare per aiutare a renderlo migliore! @@ -138,7 +140,7 @@ Puoi personalizzare l'aspetto delle tue `Stats Card` o delle `Repo Card` in qual - `bg_color` - Colore dello sfondo _(in esadecimale)_ **oppure** un gradiente nella forma _angolo,inizio,fine_ - `hide_border` - Nasconde il bordo della carta _(booleano)_ - `theme` - Nome del tema, dai un'occhiata a [tutti i temi disponibili](../themes/README.md) -- `cache_seconds` - Specifica manualmente il valore di cache, in secondi _(min: 1800, max: 86400)_ +- `cache_seconds` - Specifica manualmente il valore di cache, in secondi _(min: 14400, max: 86400)_ - `locale` - Impostare la lingua nella scheda _(per esempio. cn, de, es, eccetera.)_ ##### Gradiente nello sfondo @@ -169,7 +171,7 @@ Puoi fornire valori separati da virgola nel parametro bg_color per creare un gra - `hide` - Nasconde un linguaggio specifico _(valori separati da virgola)_ - `hide_title` - Nasconde il titolo _(booleano)_ -- `layout` - Specifica il tipo di layout, `default` (esteso) o `compact` (compatto) +- `layout` - Specificare il tipo di layout, `normal` (esteso), `compact` (compatto), `donut` (ciambella), `donut-vertical` (ciambella verticale) e `pie` (torta) - `card_width` - Specifica il valore della larghezza _(numero)_ > :warning: **Importante:** diff --git a/docs/readme_ja.md b/docs/readme_ja.md index b00c77a7712a1f..4b1dcd2bcf314c 100644 --- a/docs/readme_ja.md +++ b/docs/readme_ja.md @@ -53,6 +53,8 @@ Nederlands . नेपाली + . + Türkçe

このプロジェクトを気に入っていただけましたか?
もしよろしければ、プロジェクトのさらなる改善のために寄付を検討して頂けると嬉しいです!

@@ -139,7 +141,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr - `bg_color` - 背景の色 _(16 進数カラーコード)_ **または** _angle,start,end_ の形式でグラデーションを指定することも可 - `hide_border` - カードの境界線を非表示にします _(ブール値)_ - `theme` - [使用可能なテーマ一覧](../themes/README.md) から選んだテーマ名 -- `cache_seconds` - キャッシュ時間の秒数 _(最小値: 1800, 最大値: 86400)_ +- `cache_seconds` - キャッシュ時間の秒数 _(最小値: 14400, 最大値: 86400)_ - `locale` - カードに言語を設定する _(例えば cn, de, es, 等)_ ##### bg_color の グラデーション指定 @@ -172,7 +174,7 @@ bg_color オプションで複数のカンマ区切りの値を指定してグ - `hide` - 特定の言語を隠す _(カンマ区切りで指定)_ - `hide_title` - _(boolean)_ -- `layout` - `default` か `compact` のいずれかのレイアウトに切り替える +- `layout` - `normal` & `compact` & `donut` & `donut-vertical` & `pie` のいずれかのレイアウトに切り替える - `card_width` - カードの横幅 _(number)_ - `langs_count` - 表示される言語の数 _(1 ~ 10, 初期値 5)_ - `exclude_repo` - 指定されたリポジトリを除外する _(カンマ区切りで指定)_ diff --git a/docs/readme_kr.md b/docs/readme_kr.md index ce0b4ad0379eed..6113eedc00d102 100644 --- a/docs/readme_kr.md +++ b/docs/readme_kr.md @@ -53,6 +53,8 @@ Nederlands . नेपाली + . + Türkçe

기능들이 마음에 드시나요? 괜찮으시다면, 서비스 개선을 위해 기부를 고려해주세요! @@ -149,7 +151,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr - `bg_color` - 카드의 배경 색상 _(hex color)_ **혹은** 다음 양식으로 그라데이션 주기 _angle,start,end_ - `hide_border` - 카드의 테두리 표시 여부 _(boolean)_ - `theme` - 테마의 이름, [사용 가능한 모든 테마](../themes/README.md) 에서 선택 -- `cache_seconds` - 수동으로 캐시 헤더 설정 _(min: 1800, max: 86400)_ +- `cache_seconds` - 수동으로 캐시 헤더 설정 _(min: 14400, max: 86400)_ - `locale` - 카드에 표시할 언어 _(e.g. kr, cn, de, es, etc.)_ ##### 배경에 그라데이션 주기 @@ -187,7 +189,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr - `hide` - 카드에서 특정 언어 제외 _(Comma-separated values)_ - `hide_title` - 타이틀 제외 _(boolean)_ -- `layout` - 사용 가능한 두 가지 값, `default` & `compact` 중 표시 형태 선택 +- `layout` - 5가지 값 사용 가능, `normal` & `compact` & `donut` & `donut-vertical` & `pie` 중 표시 형태 선택 - `card_width` - 카드 너비 직접 설정 _(number)_ - `langs_count` - 카드에 표시할 언어의 수 (1-10 사이, 기본 값 : 5) _(number)_ - `exclude_repo` - 통계에 제외할 저장소 지정 _(Comma-separated values)_ @@ -296,18 +298,18 @@ _참고: `?username=` 속성의 값을 [Wakatime](https://wakatime.com) 계정의 사용자 명(닉네임)으로 바꿔주세요. ```md -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) ``` ### 미리보기 -[![willianrod 님의 wakatime 통계](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok 님의 wakatime 통계](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) -[![willianrod 님의 wakatime 통계](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok 님의 wakatime 통계](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats) - 컴팩트한 레이아웃 -[![willianrod 님의 wakatime 통계](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod&layout=compact)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok 님의 wakatime 통계](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok&layout=compact)](https://github.com/anuraghazra/github-readme-stats) --- @@ -357,7 +359,7 @@ _참고: - Wakatime 카드 -[![willianrod 님의 Wakatime 카드](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok 님의 Wakatime 카드](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) --- diff --git a/docs/readme_nl.md b/docs/readme_nl.md index 597f0c86445e36..8d38d06adbcd11 100644 --- a/docs/readme_nl.md +++ b/docs/readme_nl.md @@ -53,6 +53,8 @@ Nederlands . नेपाली + . + Türkçe

Bevalt het project? Doneer om het te verbeteren! @@ -142,7 +144,7 @@ Je kan het uiterlijk van je `Statistieken kaart` of `Repo kaart` aanpassen hoe j - `bg_color` - Achtergrond kleur van de kaart _(hex kleur)_ **of** een verloop van kleuren in het formaat van _graden,start,einde_ - `hide_border` - Verbergt de rand van de kaart _(boolean)_ - `theme` - Naam van het thema, kies uit [alle beschikbare thema\'s](../themes/README.md) -- `cache_seconds` - Stel de cache header handmatig in _(min: 1800, max: 86400)_ +- `cache_seconds` - Stel de cache header handmatig in _(min: 14400, max: 86400)_ - `locale` - Stel taal van de kaart in _(e.g. cn, de, es, etc.)_ ##### Kleurenverloop in bg_color (achtergrond kleur): @@ -174,7 +176,7 @@ Je kan meerdere komma verdeelde waarden in de bg_color optie geven om een kleure - `hide` - Verbergt specifieke talen van de kaart _(komma gescheiden waardes)_ - `hide_title` - _(boolean)_ -- `layout` - Keuze voor de twee beschikbare layouts `default` & `compact` +- `layout` - Kies uit de vijf beschikbare lay-outs `normal` & `compact` & `donut` & `donut-vertical` & `pie` - `card_width` - Stelt de breedte van de kaart handmatig in. _(nummer)_ - `langs_count` - Laat meer talen op de kaart zien, waarde tussen 1-10, staat standaard op to 5 _(nummer)_ - `exclude_repo` - Verbergt specifieke repositories _(komma gescheiden waardes)_ @@ -281,14 +283,14 @@ Je kan de `&layout=compact` optie gebruiken om het kaart ontwerp aan te passen. Verander de `?username=` waarde naar je [Wakatime](https://wakatime.com) gebruikersnaam. ```md -[![willianrod's Wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's Wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) ``` ### Demo -[![willianrod's Wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's Wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) -[![willianrod's Wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's Wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats) --- @@ -338,7 +340,7 @@ Kies uit de [standaard thema\'s](#themes) - Wakatime kaart -[![willianrod's Wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's Wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) --- diff --git a/docs/readme_np.md b/docs/readme_np.md index 654427fbda6549..e4914263019034 100644 --- a/docs/readme_np.md +++ b/docs/readme_np.md @@ -49,8 +49,12 @@ Italiano · 한국어 + . + Nederlands · नेपाली + . + Türkçe

परियोजना मनपर्‍यो? तपाईं मद्दत गर्न सक्नुहुन्छ यो परियोजना बढ्न @@ -138,7 +142,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr - `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: 14400, max: 86400)_ - `locale` - set the language in the card _(e.g. cn, de, es, etc.)_ ##### Gradient in bg_color @@ -170,7 +174,7 @@ You can provide multiple comma-separated values in bg_color option to render a g - `hide` - Hide the languages specified from the card _(Comma-separated values)_ - `hide_title` - _(boolean)_ -- `layout` - Switch between two available layouts `default` & `compact` +- `layout` - Switch between five available layouts `normal` & `compact` & `donut` & `donut-vertical` & `pie`. Default: `normal`. - `card_width` - Set the card's width manually _(number)_ - `langs_count` - Show more languages on the card, between 1-10, defaults to 5 _(number)_ - `exclude_repo` - Exclude specified repositories _(Comma-separated values)_ @@ -275,14 +279,14 @@ You can use the `&langs_count=` option to increase or decrease the number of lan Change the `?username=` value to your [Wakatime](https://wakatime.com) username. ```md -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) ``` ### डेमो -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats) --- @@ -332,7 +336,7 @@ 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) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) --- diff --git a/docs/readme_pt-BR.md b/docs/readme_pt-BR.md index 62c23dc55c7e2b..a590440ef69ec0 100644 --- a/docs/readme_pt-BR.md +++ b/docs/readme_pt-BR.md @@ -141,7 +141,7 @@ Personalize a aparência do seu `Stats Card` ou `Repo Card` da maneira que desej - `bg_color` - Cor de fundo do cartão _(hex color)_ - `hide_border` - Esconde a borda do cartão _(boleano)_ - `theme` - Nome do tema, escolha em [todos os temas disponíveis](../themes/README.md) -- `cache_seconds` - Defina o cabeçalho do cache manualmente _(min: 1800, max: 86400)_ +- `cache_seconds` - Defina o cabeçalho do cache manualmente _(min: 14400, max: 86400)_ - `locale` - defina o idioma no cartão _(por exemplo. cn, de, es, etc.)_ > Nota sobre o cache: Cartões de repositório tem um cache padrão de 30 minutos (1800 segundos), se o número a contagem de forks e contagem de estrelas é menor que 1 mil o padrão é 2 horas (7200 segundos). Note também que o cache é limitado a um mínimo de 30 minutos e um máximo de 24 horas. @@ -164,7 +164,7 @@ Personalize a aparência do seu `Stats Card` ou `Repo Card` da maneira que desej - `hide` - Oculta linguagens específicas _(Valores separados por vírgulas)_ - `hide_title` - Oculta o título _(boolean)_ -- `layout` - Alterna entre os dois layouts disponíveis `default` & `compact` +- `layout` - Alternar entre os cinco layouts disponíveis `normal` & `compact` & `donut` & `donut-vertical` & `pie` - `card_width` - Define a largura do cartão manualmente _(number)_ > :warning: **Importante:** @@ -242,14 +242,14 @@ Utilize a opção `&layout=compact` para mudar o layout do cartão. Altere o valor de `?username=` para o seu username do Wakatime. ```md -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) ``` ### Demonstração -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats) --- diff --git a/docs/readme_tr.md b/docs/readme_tr.md index 7b11cf3706f1e1..9ee358b97bdece 100644 --- a/docs/readme_tr.md +++ b/docs/readme_tr.md @@ -143,7 +143,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr - `bg_color` - Kartın arkaplan rengi _(hex color / hex rengi)_ **ya da** gradient şeklinde _açı,başlangıç,bitiş_ - `hide_border` - Kartın çerçevelerini gizler _(boolean)_ - `theme` - Temanın rengi [tüm temalar](./themes/README.md) -- `cache_seconds` - Manuel olarak cache'i belirleyebilirsiniz _(en az: 1800, en fazla: 86400)_ +- `cache_seconds` - Manuel olarak cache'i belirleyebilirsiniz _(en az: 14400, en fazla: 86400)_ - `locale` - Karttaki dili seçebilirsiniz _(örneğin; tr, cn, de, es, vb.)_ ##### bg_color'da Gradient @@ -176,7 +176,7 @@ bg_color içerisinde birden fazla rengi gradient olarak göstermek için virgül - `hide` - Belirli bir dili listede gizler _(Virgül ile ayırılmış değerlerle)_ - `hide_title` - _(boolean)_ -- `layout` - Uygun olan iki tasarım / layout arasında değişiklik yapar `default` & `compact` +- `layout` - Beş uygun tasarım / düzen arasında geçiş yapın `normal` & `compact` & `donut` & `donut-vertical` & `pie` - `card_width` - Kartın genişliğini manuel olarak belirler _(number)_ - `langs_count` - 1-10 arasında istediğiniz kadar dil gösterebilirsiniz. Varsayılan: 5 _(number)_ - `exclude_repo` - Belirli repoları listeden çıkartır _(Virgül ile ayırılmış değerlerle)_ @@ -282,18 +282,18 @@ Endpoint: `api/top-langs?username=mustafacagri` `?username=` değerini [Wakatime](https://wakatime.com)'daki kullanıcı adınızla değiştirin. ```md -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) ``` ### Demo -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats) - Kompakt Düzen -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod&layout=compact)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok&layout=compact)](https://github.com/anuraghazra/github-readme-stats) --- @@ -343,7 +343,7 @@ Endpoint: `api/top-langs?username=mustafacagri` - Wakatime kart -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) --- @@ -409,7 +409,7 @@ Teşekkürler! :heart: --- -[![https://vercel.com?utm_source=github_readme_stats_team&utm_campaign=oss](./powered-by-vercel.svg)](https://vercel.com?utm_source=github_readme_stats_team&utm_campaign=oss) +[![https://vercel.com?utm_source=github_readme_stats_team&utm_campaign=oss](../powered-by-vercel.svg)](https://vercel.com?utm_source=github_readme_stats_team&utm_campaign=oss) Katkılara açığız! <3 diff --git a/express.js b/express.js new file mode 100644 index 00000000000000..6ce92ff0351817 --- /dev/null +++ b/express.js @@ -0,0 +1,15 @@ +import statsCard from "./api/index.js"; +import repoCard from "./api/pin.js"; +import langCard from "./api/top-langs.js"; +import wakatimeCard from "./api/wakatime.js"; +import express from "express"; +import dotenv from "dotenv"; + +dotenv.config(); +const app = express(); +app.listen(process.env.port || 9000); + +app.get("/", statsCard); +app.get("/pin", repoCard); +app.get("/top-langs", langCard); +app.get("/wakatime", wakatimeCard); diff --git a/package-lock.json b/package-lock.json index 048c316bfde58f..a2156910681da0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@testing-library/dom": "^8.17.1", "@testing-library/jest-dom": "^5.16.5", "@uppercod/css-to-object": "^1.1.1", - "axios-mock-adapter": "^1.18.1", + "axios-mock-adapter": "^1.21.2", "color-contrast-checker": "^2.1.0", "hjson": "^3.2.2", "husky": "^8.0.0", @@ -1495,31 +1495,19 @@ } }, "node_modules/acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", "dev": true, "dependencies": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" - } - }, - "node_modules/acorn-globals/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" } }, "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true, "engines": { "node": ">=0.4.0" @@ -1778,12 +1766,6 @@ "node": ">=8" } }, - "node_modules/browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", - "dev": true - }, "node_modules/browserslist": { "version": "4.21.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", @@ -2509,20 +2491,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -3865,18 +3833,18 @@ } }, "node_modules/jsdom": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.0.tgz", - "integrity": "sha512-x4a6CKCgx00uCmP+QakBDFXwjAJ69IkkIWHmtmjd3wvXPcdOS44hfX2vqkOQrVrq8l9DhNNADZRXaCEWvgXtVA==", + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-pksjj7Rqoa+wdpkKcLzQRHhJCEE42qQhl/xLMUKHgoSejaKOdaXEAnqs6uDNwMl/fciHTzKeR8Wm8cw7N+g98A==", "dev": true, "dependencies": { "abab": "^2.0.6", - "acorn": "^8.7.1", - "acorn-globals": "^6.0.0", + "acorn": "^8.8.0", + "acorn-globals": "^7.0.0", "cssom": "^0.5.0", "cssstyle": "^2.3.0", "data-urls": "^3.0.2", - "decimal.js": "^10.3.1", + "decimal.js": "^10.4.1", "domexception": "^4.0.0", "escodegen": "^2.0.0", "form-data": "^4.0.0", @@ -3884,18 +3852,17 @@ "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.1", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "^7.0.0", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", + "tough-cookie": "^4.1.2", "w3c-xmlserializer": "^3.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^2.0.0", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^11.0.0", - "ws": "^8.8.0", + "ws": "^8.9.0", "xml-name-validator": "^4.0.0" }, "engines": { @@ -5397,15 +5364,6 @@ "node": ">=10.12.0" } }, - "node_modules/w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "dev": true, - "dependencies": { - "browser-process-hrtime": "^1.0.0" - } - }, "node_modules/w3c-xmlserializer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz", @@ -5581,9 +5539,9 @@ "dev": true }, "node_modules/yaml": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz", - "integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", "dev": true, "engines": { "node": ">= 14" @@ -6830,27 +6788,19 @@ "dev": true }, "acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", "dev": true, "requires": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" - }, - "dependencies": { - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - } + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" } }, "acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true }, "agent-base": { @@ -7049,12 +6999,6 @@ "fill-range": "^7.0.1" } }, - "browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", - "dev": true - }, "browserslist": { "version": "4.21.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", @@ -7590,13 +7534,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -8609,18 +8546,18 @@ } }, "jsdom": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.0.tgz", - "integrity": "sha512-x4a6CKCgx00uCmP+QakBDFXwjAJ69IkkIWHmtmjd3wvXPcdOS44hfX2vqkOQrVrq8l9DhNNADZRXaCEWvgXtVA==", + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-pksjj7Rqoa+wdpkKcLzQRHhJCEE42qQhl/xLMUKHgoSejaKOdaXEAnqs6uDNwMl/fciHTzKeR8Wm8cw7N+g98A==", "dev": true, "requires": { "abab": "^2.0.6", - "acorn": "^8.7.1", - "acorn-globals": "^6.0.0", + "acorn": "^8.8.0", + "acorn-globals": "^7.0.0", "cssom": "^0.5.0", "cssstyle": "^2.3.0", "data-urls": "^3.0.2", - "decimal.js": "^10.3.1", + "decimal.js": "^10.4.1", "domexception": "^4.0.0", "escodegen": "^2.0.0", "form-data": "^4.0.0", @@ -8628,18 +8565,17 @@ "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.1", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "^7.0.0", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", + "tough-cookie": "^4.1.2", "w3c-xmlserializer": "^3.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^2.0.0", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^11.0.0", - "ws": "^8.8.0", + "ws": "^8.9.0", "xml-name-validator": "^4.0.0" } }, @@ -9724,15 +9660,6 @@ "convert-source-map": "^1.6.0" } }, - "w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "dev": true, - "requires": { - "browser-process-hrtime": "^1.0.0" - } - }, "w3c-xmlserializer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz", @@ -9855,9 +9782,9 @@ "dev": true }, "yaml": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz", - "integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", "dev": true }, "yargs": { diff --git a/package.json b/package.json index 95b1a11dad93f3..606d4f5440a242 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@testing-library/dom": "^8.17.1", "@testing-library/jest-dom": "^5.16.5", "@uppercod/css-to-object": "^1.1.1", - "axios-mock-adapter": "^1.18.1", + "axios-mock-adapter": "^1.21.2", "color-contrast-checker": "^2.1.0", "hjson": "^3.2.2", "husky": "^8.0.0", diff --git a/readme.md b/readme.md index 678c5c0b14af48..e7ef2b7096b5c3 100644 --- a/readme.md +++ b/readme.md @@ -66,7 +66,7 @@

Love the project? Please consider donating to help it improve!

- Give india logo + Give india logo Are you considering supporting the project by donating? Please DO NOT!! @@ -92,7 +92,9 @@ Visit and make a small donation to hel - [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) +- [Deploy Yourself](#deploy-on-your-own) + - [On Vercel](#on-vercel) + - [On other platforms](#on-other-platforms) - [Keep your fork up to date](#keep-your-fork-up-to-date) # GitHub Stats Card @@ -118,22 +120,9 @@ You can pass a query parameter `&hide=` to hide any specific stats with comma-se ![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=contribs,prs) ``` -### 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`. - -> **Note** -> If you are deploying this project yourself, the private contributions will be counted by default. If you are using the public Vercel instance, you need to choose to [share your private contributions](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-github-profile/managing-contribution-settings-on-your-profile/showing-your-private-contributions-and-achievements-on-your-profile). - -> Options: `&count_private=true` - -```md -![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&count_private=true) -``` - ### Showing icons -To enable icons, you can pass `show_icons=true` in the query param, like so: +To enable icons, you can pass `&show_icons=true` in the query param, like so: ```md ![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true) @@ -181,7 +170,7 @@ We have included a `transparent` theme that has a transparent background. This t ##### Add transparent alpha channel to a themes bg_color -You can use the `bg_color` parameter to make any of [the available themes](./themes/README.md) transparent. This is done by setting the `bg_color` to a colour with a transparent alpha channel (i.e. `bg_color=00000000`): +You can use the `bg_color` parameter to make any of [the available themes](./themes/README.md) transparent. This is done by setting the `bg_color` to a color with a transparent alpha channel (i.e. `bg_color=00000000`): ```md ![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&bg_color=00000000) @@ -214,10 +203,10 @@ You can use [GitHub's theme context](https://github.blog/changelog/2021-11-24-sp ##### Use GitHub's new media feature You can use [GitHub's new media feature](https://github.blog/changelog/2022-05-19-specify-theme-context-for-images-in-markdown-beta/) in HTML to specify whether to display images for light or dark themes. This is done using the HTML `` element in combination with the `prefers-color-scheme` media feature. - + ```html - @@ -233,7 +222,7 @@ You can use [GitHub's new media feature](https://github.blog/changelog/2022-05-1 :eyes: Show example - @@ -258,7 +247,7 @@ You can customize the appearance of your `Stats Card` or `Repo Card` however you - `border_color` - Card's border color _(hex color)_. Default: `e4e2e2` (Does not apply when `hide_border` is enabled). - `bg_color` - Card's background color _(hex color)_ **or** a gradient in the form of _angle,start,end_. Default: `fffefe` - `hide_border` - Hides the card's border _(boolean)_. Default: `false` -- `theme` - name of the theme, choose from [all available themes](./themes/README.md). Default: `default` theme. +- `theme` - name of the theme, choose from [all available themes](./themes/README.md). Default: `default` theme. - `cache_seconds` - set the cache header manually _(min: 14400, max: 86400)_. Default: `14400 seconds (4 hours)`. - `locale` - set the language in the card _(e.g. cn, de, es, etc.)_. Default: `en`. - `border_radius` - Corner rounding on the card. Default: `4.5`. @@ -278,15 +267,17 @@ You can provide multiple comma-separated values in the bg_color option to render - `hide_title` - _(boolean)_. Default: `false`. - `card_width` - Set the card's width manually _(number)_. Default: `500px (approx.)`. - `hide_rank` - _(boolean)_ hides the rank and automatically resizes the card width. Default: `false`. +- `rank_icon` - Shows alternative rank icon (i.e. `github` or `default`). Default: `default`. - `show_icons` - _(boolean)_. Default: `false`. - `include_all_commits` - Count total commits instead of just the current year commits _(boolean)_. Default: `false`. -- `count_private` - Count private commits _(boolean)_. Default: `false`. +- `count_private` - Count private contributions _(boolean)_. Default: `false`. - `line_height` - Sets the line height between text _(number)_. Default: `25`. - `exclude_repo` - Exclude stars from specified repositories _(Comma-separated values)_. Default: `[] (blank array)`. - `custom_title` - Sets a custom title for the card. Default: ` GitHub Stats`. - `text_bold` - Use bold text _(boolean)_. Default: `true`. - `disable_animations` - Disables all animations in the card _(boolean)_. Default: `false`. - `ring_color` - Color of the rank circle _(hex color)_. Defaults to the theme ring color if it exists and otherwise the title color. +- `number_format` - Switch between two available formats for displaying the card values `short` (i.e. `6.6k`) and `long` (i.e. `6626`). Default: `short`. > **Note** > When hide_rank=`true`, the minimum card width is 270 px + the title length and padding. @@ -299,12 +290,15 @@ You can provide multiple comma-separated values in the bg_color option to render - `hide` - Hide the languages specified from the card _(Comma-separated values)_. Default: `[] (blank array)`. - `hide_title` - _(boolean)_. Default: `false`. -- `layout` - Switch between two available layouts `default` & `compact`. Default: `default`. +- `layout` - Switch between five available layouts `normal` & `compact` & `donut` & `donut-vertical` & `pie`. Default: `normal`. - `card_width` - Set the card's width manually _(number)_. Default `300`. - `langs_count` - Show more languages on the card, between 1-10 _(number)_. Default `5`. - `exclude_repo` - Exclude specified repositories _(Comma-separated values)_. Default: `[] (blank array)`. - `custom_title` - Sets a custom title for the card _(string)_. Default `Most Used Languages`. - `disable_animations` - Disables all animations in the card _(boolean)_. Default: `false`. +- `hide_progress` - It uses the compact layout option, hides percentages, and removes the bars. Default: `false`. +- `size_weight` - Configures language stats algorithm _(number)_ (see [Language stats algorithm](#Language-stats-algorithm)), defaults to 1. +- `count_weight` - Configures language stats algorithm _(number)_ (see [Language stats algorithm](#Language-stats-algorithm)), defaults to 0. > **Warning** > Language names should be URI-escaped, as specified in [Percent Encoding](https://en.wikipedia.org/wiki/Percent-encoding) @@ -321,7 +315,6 @@ You can provide multiple comma-separated values in the bg_color option to render - `layout` - Switch between two available layouts `default` & `compact`. Default `default`. - `langs_count` - Limit the number of languages on the card, defaults to all reported languages _(number)_. - `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) _(string)_. Default `Waka API`. -- `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. _(YYYY-MM, last_7_days, last_30_days, last_6_months, last_year, or all_time)_. Default `all_time`. * * * @@ -354,7 +347,7 @@ Use [show_owner](#customization) variable to include the repo's owner username 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 to determine which languages have the most code on GitHub. It is a new feature of github-readme-stats. +> Top Languages does not indicate the user's skill level or anything like that; it's a GitHub metric to determine which languages have the most code on GitHub. It is a new feature of github-readme-stats. ### Usage @@ -366,6 +359,24 @@ Endpoint: `api/top-langs?username=anuraghazra` [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats) ``` +### Language stats algorithm + +We use the following algorithm to calculate the languages percentages on the language card: + +```js +ranking_index = (byte_count ^ size_weight) * (repo_count ^ count_weight) +``` + +By default, only the byte count is used for determining the languages percentages shown on the language card (i.e. `size_weight=1` and `count_weight=0`). You can, however, use the `&size_weight=` and `&count_weight=` options to weight the language usage calculation. The values must be positive real numbers. [More details about the algorithm can be found here](https://github.com/anuraghazra/github-readme-stats/issues/1600#issuecomment-1046056305). + +- `&size_weight=1&count_weight=0` - _(default)_ Orders by byte count. +- `&size_weight=0.5&count_weight=0.5` - _(recommended)_ Uses both byte and repo count for ranking +- `&size_weight=0&count_weight=1` - Orders by repo count + +```md +[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&size_weight=0.5&count_weight=0.5)](https://github.com/anuraghazra/github-readme-stats) +``` + ### Exclude individual repositories You can use the `&exclude_repo=repo1,repo2` parameter to exclude individual repositories. @@ -398,6 +409,38 @@ You can use the `&layout=compact` option to change the card design. [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=compact)](https://github.com/anuraghazra/github-readme-stats) ``` +### Donut Chart Language Card Layout + +You can use the `&layout=donut` option to change the card design. + +```md +[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=donut)](https://github.com/anuraghazra/github-readme-stats) +``` + +### Donut Vertical Chart Language Card Layout + +You can use the `&layout=donut-vertical` option to change the card design. + +```md +[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=donut-vertical)](https://github.com/anuraghazra/github-readme-stats) +``` + +### Pie Chart Language Card Layout + +You can use the `&layout=pie` option to change the card design. + +```md +[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=pie)](https://github.com/anuraghazra/github-readme-stats) +``` + +### Hide Progress Bars + +You can use the `&hide_progress=true` option to hide the percentages and the progress bars (layout will be automatically set to `compact`). + +```md +[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats) +``` + ### Demo [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats) @@ -406,26 +449,42 @@ You can use the `&layout=compact` option to change the card design. [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=compact)](https://github.com/anuraghazra/github-readme-stats) +- Donut Chart layout + +[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=donut)](https://github.com/anuraghazra/github-readme-stats) + +- Donut Vertical Chart layout + +[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=donut-vertical)](https://github.com/anuraghazra/github-readme-stats) + +- Pie Chart layout + +[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=pie)](https://github.com/anuraghazra/github-readme-stats) + +- Hidden progress bars + +[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats) + # Wakatime Week Stats +> **Warning** +> Please be aware that we currently only show data from Wakatime profiles that are public. You therefore have to make sure that **BOTH** `Display code time publicly` and `Display languages, editors, os, categories publicly` are enabled. + Change the `?username=` value to your [Wakatime](https://wakatime.com) username. ```md -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](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) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats) - Compact layout -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod&layout=compact)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok&layout=compact)](https://github.com/anuraghazra/github-readme-stats) * * * @@ -443,6 +502,10 @@ Change the `?username=` value to your [Wakatime](https://wakatime.com) username. ![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&hide=issues&show_icons=true) +- Shows Github logo instead rank level + +![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&rank_icon=github) + - Customize Border Color ![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=anuraghazra&border_color=2e4058) @@ -479,7 +542,7 @@ Choose from any of the [default themes](#themes) - WakaTime card -[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats) +[![Harlok's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=Harlok)](https://github.com/anuraghazra/github-readme-stats) * * * @@ -496,7 +559,9 @@ By default, GitHub does not lay out the cards side by side. To do that, you can ``` -## Deploy on your own Vercel instance +## Deploy on your own + +### On Vercel #### :film_projector: [Check Out Step By Step Video Tutorial By @codeSTACKr](https://youtu.be/n6d4KHSKqGk?t=107) @@ -533,6 +598,23 @@ Since the GitHub API only allows 5k requests per hour, my `https://github-readme +### On other platforms + +> **Warning** +> This way of using GRS is not officially supported and was added to cater to some particular use cases where Vercel could not be used (e.g. #2341). The support for this method, therefore, is limited. + +
+:hammer_and_wrench: Step-by-step guide for deploying on other platforms + +1. Fork or clone this repo as per your needs +2. Add `express` to the dependencies section of `package.json` + +3. Run `npm i` if needed (initial setup) +4. Run `node express.js` to start the server, or set the entry point to `express.js` in `package.json` if you're deploying on a managed service + +5. You're done 🎉 +
+ ### Keep your fork up to date You can keep your fork, and thus your private Vercel instance up to date with the upstream using GitHubs' [Sync Fork button](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork). You can also use the [pull](https://github.com/wei/pull) package created by [@wei](https://github.com/wei) to automate this process. diff --git a/scripts/preview-theme.js b/scripts/preview-theme.js index 38faf873ce3d5e..57b792a369c958 100644 --- a/scripts/preview-theme.js +++ b/scripts/preview-theme.js @@ -26,9 +26,9 @@ const FAIL_TEXT = ` \rUnfortunately, your theme PR contains an error or does not adhere to our [theme guidelines](https://github.com/anuraghazra/github-readme-stats/blob/master/CONTRIBUTING.md#themes-contribution). Please fix the issues below, and we will review your\ \r PR again. This pull request will **automatically close in 20 days** if no changes are made. After this time, you must re-open the PR for it to be reviewed. `; -const THEME_CONTRIB_GUIDELINESS = ` +const THEME_CONTRIB_GUIDELINES = ` \rHi, thanks for the theme contribution. Please read our theme [contribution guidelines](https://github.com/anuraghazra/github-readme-stats/blob/master/CONTRIBUTING.md#themes-contribution). - \rWe are currently only accepting color combinations from any VSCode theme or themes with good colour combinations to minimize bloating the themes collection. + \rWe are currently only accepting color combinations from any VSCode theme or themes with good color combinations 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). `; @@ -43,6 +43,23 @@ const ACCEPTED_COLOR_PROPS = Object.keys(COLOR_PROPS); const REQUIRED_COLOR_PROPS = ACCEPTED_COLOR_PROPS.slice(0, 4); const INVALID_REVIEW_COMMENT = (commentUrl) => `Some themes are invalid. See the [Automated Theme Preview](${commentUrl}) comment above for more information.`; +var OCTOKIT; +var OWNER; +var REPO; +var PULL_REQUEST_ID; + +/** + * Incorrect JSON format error. + * @extends Error + * @param {string} message Error message. + * @returns {Error} IncorrectJsonFormatError. + */ +class IncorrectJsonFormatError extends Error { + constructor(message) { + super(message); + this.name = "IncorrectJsonFormatError"; + } +} /** * Retrieve PR number from the event payload. @@ -126,15 +143,36 @@ const findComment = async (octokit, issueNumber, owner, repo, commenter) => { * Create or update the preview comment. * * @param {Object} octokit Octokit instance. - * @param {Object} props Comment properties. + * @param {number} issueNumber Issue number. + * @param {Object} repo Repository name. + * @param {Object} owner Owner of the repository. + * @param {number} commentId Comment ID. + * @param {string} body Comment body. * @return {string} The comment URL. */ -const upsertComment = async (octokit, props) => { +const upsertComment = async ( + octokit, + issueNumber, + repo, + owner, + commentId, + body, +) => { let resp; - if (props.comment_id !== undefined) { - resp = await octokit.issues.updateComment(props); + if (commentId !== undefined) { + resp = await octokit.issues.updateComment({ + owner, + repo, + comment_id: commentId, + body, + }); } else { - resp = await octokit.issues.createComment(props); + resp = await octokit.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body, + }); } return resp.data.html_url; }; @@ -269,22 +307,38 @@ const parseJSON = (json) => { if (typeof parsedJson === "object") { return parsedJson; } else { - throw new Error("PR diff is not a valid theme JSON object."); + throw new IncorrectJsonFormatError( + "PR diff is not a valid theme JSON object.", + ); } } catch (error) { - let parsedJson = json + // Remove trailing commas (if any). + let parsedJson = json.replace(/(,\s*})/g, "}"); + + // Remove JS comments (if any). + parsedJson = parsedJson.replace(/\/\/[A-z\s]*\s/g, ""); + + // Fix incorrect open bracket (if any). + const splitJson = parsedJson .split(/([\s\r\s]*}[\s\r\s]*,[\s\r\s]*)(?=[\w"-]+:)/) - .filter((x) => typeof x !== "string" || !!x.trim()); - if (parsedJson[0].replace(/\s+/g, "") === "},") { - parsedJson[0] = "},"; - if (!/\s*}\s*,?\s*$/.test(parsedJson[1])) { - parsedJson.push(parsedJson.shift()); + .filter((x) => typeof x !== "string" || !!x.trim()); // Split json into array of strings and objects. + if (splitJson[0].replace(/\s+/g, "") === "},") { + splitJson[0] = "},"; + if (!/\s*}\s*,?\s*$/.test(splitJson[1])) { + splitJson.push(splitJson.shift()); } else { - parsedJson.shift(); + splitJson.shift(); } - return Hjson.parse(parsedJson.join("")); - } else { - throw error; + parsedJson = splitJson.join(""); + } + + // Try to parse the fixed json. + try { + return Hjson.parse(parsedJson); + } catch (error) { + throw new IncorrectJsonFormatError( + `Theme JSON file could not be parsed: ${error.message}`, + ); } } }; @@ -298,53 +352,64 @@ const themeNameAlreadyExists = (name) => { return themes[name] !== undefined; }; +const DRY_RUN = process.env.DRY_RUN === "true" || false; + /** * Main function. */ -export const run = async (prNumber) => { +export const run = async () => { try { - const dryRun = process.env.DRY_RUN === "true" || false; debug("Retrieve action information from context..."); debug(`Context: ${inspect(github.context)}`); let commentBody = ` \r# ${COMMENT_TITLE} - \r${THEME_CONTRIB_GUIDELINESS} + \r${THEME_CONTRIB_GUIDELINES} `; const ccc = new ColorContrastChecker(); - const octokit = github.getOctokit(getGithubToken()); - const pullRequestId = prNumber ? prNumber : getPrNumber(); - const commenter = getCommenter(); + OCTOKIT = github.getOctokit(getGithubToken()); + PULL_REQUEST_ID = getPrNumber(); const { owner, repo } = getRepoInfo(github.context); - debug(`Owner: ${owner}`); - debug(`Repo: ${repo}`); + OWNER = owner; + REPO = repo; + const commenter = getCommenter(); + PULL_REQUEST_ID = getPrNumber(); + debug(`Owner: ${OWNER}`); + debug(`Repo: ${REPO}`); debug(`Commenter: ${commenter}`); // Retrieve the PR diff and preview-theme comment. debug("Retrieve PR diff..."); - const res = await octokit.pulls.get({ - owner, - repo, - pull_number: pullRequestId, + const res = await OCTOKIT.pulls.get({ + owner: OWNER, + repo: REPO, + pull_number: PULL_REQUEST_ID, mediaType: { format: "diff", }, }); debug("Retrieve preview-theme comment..."); const comment = await findComment( - octokit, - pullRequestId, - owner, - repo, + OCTOKIT, + PULL_REQUEST_ID, + OWNER, + REPO, commenter, ); // Retrieve theme changes from the PR diff. debug("Retrieve themes..."); const diff = parse(res.data); + + // Retrieve all theme changes from the PR diff and convert to JSON. + debug("Retrieve theme changes..."); const content = diff .find((file) => file.to === "themes/index.js") - .chunks[0].changes.filter((c) => c.type === "add") - .map((c) => c.content.replace("+", "")) + .chunks.map((chunk) => + chunk.changes + .filter((c) => c.type === "add") + .map((c) => c.content.replace("+", "")) + .join(""), + ) .join(""); const themeObject = parseJSON(content); if ( @@ -513,14 +578,15 @@ export const run = async (prNumber) => { // Create or update theme-preview comment. debug("Create or update theme-preview comment..."); let comment_url; - if (!dryRun) { - comment_url = await upsertComment(octokit, { - comment_id: comment?.id, - issue_number: pullRequestId, - owner, - repo, - body: commentBody, - }); + if (!DRY_RUN) { + comment_url = await upsertComment( + OCTOKIT, + PULL_REQUEST_ID, + REPO, + OWNER, + comment?.id, + commentBody, + ); } else { info(`DRY_RUN: Comment body: ${commentBody}`); comment_url = ""; @@ -535,20 +601,20 @@ export const run = async (prNumber) => { const reviewReason = themesValid ? undefined : INVALID_REVIEW_COMMENT(comment_url); - if (!dryRun) { + if (!DRY_RUN) { await addReview( - octokit, - pullRequestId, - owner, - repo, + OCTOKIT, + PULL_REQUEST_ID, + OWNER, + REPO, reviewState, reviewReason, ); await addRemoveLabel( - octokit, - pullRequestId, - owner, - repo, + OCTOKIT, + PULL_REQUEST_ID, + OWNER, + REPO, "invalid", !themesValid, ); @@ -558,20 +624,22 @@ export const run = async (prNumber) => { } } catch (error) { debug("Set review state to `REQUEST_CHANGES` and add `invalid` label..."); - if (!dryRun) { + if (!DRY_RUN) { await addReview( - octokit, - pullRequestId, - owner, - repo, + OCTOKIT, + PULL_REQUEST_ID, + OWNER, + REPO, "REQUEST_CHANGES", - error.message, + "**Something went wrong in the theme preview action:** `" + + error.message + + "`", ); await addRemoveLabel( - octokit, - pullRequestId, - owner, - repo, + OCTOKIT, + PULL_REQUEST_ID, + OWNER, + REPO, "invalid", true, ); diff --git a/scripts/push-theme-readme.sh b/scripts/push-theme-readme.sh index 1ab5de474ea5a5..132a4b508e8e48 100755 --- a/scripts/push-theme-readme.sh +++ b/scripts/push-theme-readme.sh @@ -6,6 +6,7 @@ export BRANCH_NAME=updated-theme-readme git --version git config --global user.email "no-reply@githubreadmestats.com" git config --global user.name "GitHub Readme Stats Bot" +git config --global --add safe.directory ${GITHUB_WORKSPACE} git branch -d $BRANCH_NAME || true git checkout -b $BRANCH_NAME git add --all diff --git a/src/calculateRank.js b/src/calculateRank.js index 24845bc7d99443..7648ad412ed67f 100644 --- a/src/calculateRank.js +++ b/src/calculateRank.js @@ -1,104 +1,72 @@ -/** - * Calculates the probability of x taking on x or a value less than x in a normal distribution - * with mean and standard deviation. - * - * @see https://stackoverflow.com/a/5263759/10629172 - * - * @param {string} mean The mean of the normal distribution. - * @param {number} sigma The standard deviation of the normal distribution. - * @param {number} to The value to calculate the probability for. - * @returns {number} Probability. - */ -const normalcdf = (mean, sigma, to) => { - var z = (to - mean) / Math.sqrt(2 * sigma * sigma); - var t = 1 / (1 + 0.3275911 * Math.abs(z)); - var a1 = 0.254829592; - var a2 = -0.284496736; - var a3 = 1.421413741; - var a4 = -1.453152027; - var a5 = 1.061405429; - var erf = - 1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-z * z); - var sign = 1; - if (z < 0) { - sign = -1; - } - return (1 / 2) * (1 + sign * erf); -}; +function expsf(x, lambda = 1) { + return 2 ** (-lambda * x); +} /** * Calculates the users rank. * - * @param {number} totalRepos Total number of repos. - * @param {number} totalCommits Total number of commits. - * @param {number} contributions The number of contributions. - * @param {number} followers The number of followers. - * @param {number} prs The number of pull requests. - * @param {number} issues The number of issues. - * @param {number} stargazers The number of stars. + * @param {object} params Parameters on which the user's rank depends. + * @param {boolean} params.all_commits Whether `include_all_commits` was used. + * @param {number} params.commits Number of commits. + * @param {number} params.prs The number of pull requests. + * @param {number} params.issues The number of issues. + * @param {number} params.repos Total number of repos. + * @param {number} params.stars The number of stars. + * @param {number} params.followers The number of followers. * @returns {{level: string, score: number}}} The users rank. */ -const calculateRank = ({ - totalRepos, - totalCommits, - contributions, - followers, +function calculateRank({ + all_commits, + commits, prs, issues, - stargazers, -}) => { - const COMMITS_OFFSET = 1.65; - const CONTRIBS_OFFSET = 1.65; - const ISSUES_OFFSET = 1; - const STARS_OFFSET = 0.75; - const PRS_OFFSET = 0.5; - const FOLLOWERS_OFFSET = 0.45; - const REPO_OFFSET = 1; - - const ALL_OFFSETS = - CONTRIBS_OFFSET + - ISSUES_OFFSET + - STARS_OFFSET + - PRS_OFFSET + - FOLLOWERS_OFFSET + - REPO_OFFSET; - - const RANK_S_VALUE = 1; - const RANK_DOUBLE_A_VALUE = 25; - const RANK_A2_VALUE = 45; - const RANK_A3_VALUE = 60; - const RANK_B_VALUE = 100; + repos, // unused + stars, + followers, +}) { + const COMMITS_MEAN = all_commits ? 1000 : 250, + COMMITS_WEIGHT = 2; + const PRS_MEAN = 50, + PRS_WEIGHT = 3; + const ISSUES_MEAN = 25, + ISSUES_WEIGHT = 1; + const STARS_MEAN = 250, + STARS_WEIGHT = 4; + const FOLLOWERS_MEAN = 25, + FOLLOWERS_WEIGHT = 1; - const TOTAL_VALUES = - RANK_S_VALUE + - RANK_DOUBLE_A_VALUE + - RANK_A2_VALUE + - RANK_A3_VALUE + - RANK_B_VALUE; + const TOTAL_WEIGHT = + COMMITS_WEIGHT + + PRS_WEIGHT + + ISSUES_WEIGHT + + STARS_WEIGHT + + FOLLOWERS_WEIGHT; - // prettier-ignore - const score = ( - totalCommits * COMMITS_OFFSET + - contributions * CONTRIBS_OFFSET + - issues * ISSUES_OFFSET + - stargazers * STARS_OFFSET + - prs * PRS_OFFSET + - followers * FOLLOWERS_OFFSET + - totalRepos * REPO_OFFSET - ) / 100; + const rank = + (COMMITS_WEIGHT * expsf(commits, 1 / COMMITS_MEAN) + + PRS_WEIGHT * expsf(prs, 1 / PRS_MEAN) + + ISSUES_WEIGHT * expsf(issues, 1 / ISSUES_MEAN) + + STARS_WEIGHT * expsf(stars, 1 / STARS_MEAN) + + FOLLOWERS_WEIGHT * expsf(followers, 1 / FOLLOWERS_MEAN)) / + TOTAL_WEIGHT; - const normalizedScore = normalcdf(score, TOTAL_VALUES, ALL_OFFSETS) * 100; + const RANK_S_PLUS = 0.025; + const RANK_S = 0.1; + const RANK_A_PLUS = 0.25; + const RANK_A = 0.5; + const RANK_B_PLUS = 0.75; const level = (() => { - if (normalizedScore < RANK_S_VALUE) return "S+"; - if (normalizedScore < RANK_DOUBLE_A_VALUE) return "S"; - if (normalizedScore < RANK_A2_VALUE) return "A++"; - if (normalizedScore < RANK_A3_VALUE) return "A+"; - return "B+"; + 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: normalizedScore }; -}; + return { level, score: rank * 100 }; +} export { calculateRank }; export default calculateRank; diff --git a/src/cards/stats-card.js b/src/cards/stats-card.js index f39a968f180659..4761d023e4ab1d 100644 --- a/src/cards/stats-card.js +++ b/src/cards/stats-card.js @@ -1,7 +1,7 @@ // @ts-check import { Card } from "../common/Card.js"; import { I18n } from "../common/I18n.js"; -import { icons } from "../common/icons.js"; +import { icons, rankIcon } from "../common/icons.js"; import { clampValue, flexLayout, @@ -20,14 +20,16 @@ const RANK_CARD_DEFAULT_WIDTH = 450; /** * Create a stats card text item. * - * @param {object[]} createTextNodeParams Object that contains the createTextNode parameters. + * @param {object} createTextNodeParams Object that contains the createTextNode parameters. + * @param {string} createTextNodeParams.icon The icon to display. * @param {string} createTextNodeParams.label The label to display. - * @param {string} createTextNodeParams.value The value to display. + * @param {number} createTextNodeParams.value The value to display. * @param {string} createTextNodeParams.id The id of the stat. * @param {number} createTextNodeParams.index The index of the stat. * @param {boolean} createTextNodeParams.showIcons Whether to show icons. * @param {number} createTextNodeParams.shiftValuePos Number of pixels the value has to be shifted to the right. * @param {boolean} createTextNodeParams.bold Whether to bold the label. + * @param {string} createTextNodeParams.number_format The format of numbers on card. * @returns */ const createTextNode = ({ @@ -39,8 +41,10 @@ const createTextNode = ({ showIcons, shiftValuePos, bold, + number_format, }) => { - const kValue = kFormatter(value); + const kValue = + number_format.toLowerCase() === "long" ? value : kFormatter(value); const staggerDelay = (index + 3) * 150; const labelOffset = showIcons ? `x="25"` : ""; @@ -103,8 +107,10 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => { custom_title, border_radius, border_color, + number_format = "short", locale, disable_animations = false, + rank_icon = "default", } = options; const lheight = parseInt(String(line_height), 10); @@ -179,7 +185,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => { "nl", "zh-tw", ]; - const isLongLocale = longLocales.includes(locale) === true; + const isLongLocale = longLocales.includes(locale); // filter out hidden stats defined by user & create the text nodes const statItems = Object.keys(STATS) @@ -192,6 +198,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => { showIcons: show_icons, shiftValuePos: 79.01 + (isLongLocale ? 50 : 0), bold: text_bold, + number_format, }), ); @@ -290,15 +297,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => { - - ${rank.level} - + ${rankIcon(rank_icon, rank?.level)} `; diff --git a/src/cards/top-languages-card.js b/src/cards/top-languages-card.js index 9396ff8e73d5ef..92f5b9d92d9e18 100644 --- a/src/cards/top-languages-card.js +++ b/src/cards/top-languages-card.js @@ -13,10 +13,11 @@ import { import { langCardLocales } from "../translations.js"; const DEFAULT_CARD_WIDTH = 300; -const MIN_CARD_WIDTH = 230; +const MIN_CARD_WIDTH = 280; const DEFAULT_LANGS_COUNT = 5; const DEFAULT_LANG_COLOR = "#858585"; const CARD_PADDING = 25; +const COMPACT_LAYOUT_BASE_HEIGHT = 90; /** * @typedef {import("../fetchers/types").Lang} Lang @@ -36,13 +37,164 @@ const getLongestLang = (arr) => ); /** - * Creates a node to display usage of a programming language in percentage - * using text and a horizontal progress bar. + * Convert degrees to radians. + * + * @param {number} angleInDegrees Angle in degrees. + * @returns Angle in radians. + */ +const degreesToRadians = (angleInDegrees) => angleInDegrees * (Math.PI / 180.0); + +/** + * Convert radians to degrees. + * + * @param {number} angleInRadians Angle in radians. + * @returns Angle in degrees. + */ +const radiansToDegrees = (angleInRadians) => angleInRadians / (Math.PI / 180.0); + +/** + * Convert polar coordinates to cartesian coordinates. + * + * @param {number} centerX Center x coordinate. + * @param {number} centerY Center y coordinate. + * @param {number} radius Radius of the circle. + * @param {number} angleInDegrees Angle in degrees. + * @returns {{x: number, y: number}} Cartesian coordinates. + */ +const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => { + const rads = degreesToRadians(angleInDegrees); + return { + x: centerX + radius * Math.cos(rads), + y: centerY + radius * Math.sin(rads), + }; +}; + +/** + * Convert cartesian coordinates to polar coordinates. + * + * @param {number} centerX Center x coordinate. + * @param {number} centerY Center y coordinate. + * @param {number} x Point x coordinate. + * @param {number} y Point y coordinate. + * @returns {{radius: number, angleInDegrees: number}} Polar coordinates. + */ +const cartesianToPolar = (centerX, centerY, x, y) => { + const radius = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)); + let angleInDegrees = radiansToDegrees(Math.atan2(y - centerY, x - centerX)); + if (angleInDegrees < 0) angleInDegrees += 360; + return { radius, angleInDegrees }; +}; + +/** + * Calculates length of circle. + * + * @param {number} radius Radius of the circle. + * @returns {number} The length of the circle. + */ +const getCircleLength = (radius) => { + return 2 * Math.PI * radius; +}; + +/** + * Calculates height for the compact layout. + * + * @param {number} totalLangs Total number of languages. + * @returns {number} Card height. + */ +const calculateCompactLayoutHeight = (totalLangs) => { + return COMPACT_LAYOUT_BASE_HEIGHT + Math.round(totalLangs / 2) * 25; +}; + +/** + * Calculates height for the normal layout. + * + * @param {number} totalLangs Total number of languages. + * @returns {number} Card height. + */ +const calculateNormalLayoutHeight = (totalLangs) => { + return 45 + (totalLangs + 1) * 40; +}; + +/** + * Calculates height for the donut layout. + * + * @param {number} totalLangs Total number of languages. + * @returns {number} Card height. + */ +const calculateDonutLayoutHeight = (totalLangs) => { + return 215 + Math.max(totalLangs - 5, 0) * 32; +}; + +/** + * Calculates height for the donut vertical layout. + * + * @param {number} totalLangs Total number of languages. + * @returns {number} Card height. + */ +const calculateDonutVerticalLayoutHeight = (totalLangs) => { + return 300 + Math.round(totalLangs / 2) * 25; +}; + +/** + * Calculates height for the pie layout. + * + * @param {number} totalLangs Total number of languages. + * @returns {number} Card height. + */ +const calculatePieLayoutHeight = (totalLangs) => { + return 300 + Math.round(totalLangs / 2) * 25; +}; + +/** + * Calculates the center translation needed to keep the donut chart centred. + * @param {number} totalLangs Total number of languages. + * @returns {number} Donut center translation. + */ +const donutCenterTranslation = (totalLangs) => { + return -45 + Math.max(totalLangs - 5, 0) * 16; +}; + +/** + * Trim top languages to lang_count while also hiding certain languages. + * + * @param {Record} topLangs Top languages. + * @param {string[]} hide Languages to hide. + * @param {string} langs_count Number of languages to show. + * @returns {{topLangs: Record, totalSize: number}} Trimmed top languages and total size. + */ +const trimTopLanguages = (topLangs, hide, langs_count) => { + let langs = Object.values(topLangs); + let langsToHide = {}; + let langsCount = clampValue(parseInt(langs_count), 1, 10); + + // populate langsToHide map for quick lookup + // while filtering out + if (hide) { + hide.forEach((langName) => { + langsToHide[lowercaseTrim(langName)] = true; + }); + } + + // filter out languages to be hidden + langs = langs + .sort((a, b) => b.size - a.size) + .filter((lang) => { + return !langsToHide[lowercaseTrim(lang.name)]; + }) + .slice(0, langsCount); + + const totalLanguageSize = langs.reduce((acc, curr) => acc + curr.size, 0); + + return { langs, totalLanguageSize }; +}; + +/** + * Create progress bar text item for a programming language. * * @param {object} props Function properties. * @param {number} props.width The card width - * @param {string} props.name Name of the programming language. * @param {string} props.color Color of the programming language. + * @param {string} props.name Name of the programming language. * @param {string} props.progress Usage of the programming language in percentage. * @param {number} props.index Index of the programming language. * @returns {string} Programming language SVG node. @@ -71,15 +223,16 @@ const createProgressTextNode = ({ width, color, name, progress, index }) => { }; /** - * Creates a text only node to display usage of a programming language in percentage. + * Creates compact text item for a programming language. * * @param {object} props Function properties. * @param {Lang} props.lang Programming language object. * @param {number} props.totalSize Total size of all languages. + * @param {boolean} props.hideProgress Whether to hide percentage. * @param {number} props.index Index of the programming language. * @returns {string} Compact layout programming language SVG node. */ -const createCompactLangNode = ({ lang, totalSize, index }) => { +const createCompactLangNode = ({ lang, totalSize, hideProgress, index }) => { const percentage = ((lang.size / totalSize) * 100).toFixed(2); const staggerDelay = (index + 3) * 150; const color = lang.color || "#858585"; @@ -88,21 +241,22 @@ const createCompactLangNode = ({ lang, totalSize, index }) => { - ${lang.name} ${percentage}% + ${lang.name} ${hideProgress ? "" : percentage + "%"} `; }; /** - * Creates compact layout of text only language nodes. + * Create compact languages text items for all programming languages. * - * @param {object[]} props Function properties. + * @param {object} props Function properties. * @param {Lang[]} props.langs Array of programming languages. * @param {number} props.totalSize Total size of all languages. + * @param {boolean} props.hideProgress Whether to hide percentage. * @returns {string} Programming languages SVG node. */ -const createLanguageTextNode = ({ langs, totalSize }) => { +const createLanguageTextNode = ({ langs, totalSize, hideProgress }) => { const longestLang = getLongestLang(langs); const chunked = chunkArray(langs, langs.length / 2); const layouts = chunked.map((array) => { @@ -111,6 +265,7 @@ const createLanguageTextNode = ({ langs, totalSize }) => { createCompactLangNode({ lang, totalSize, + hideProgress, index, }), ); @@ -131,7 +286,30 @@ const createLanguageTextNode = ({ langs, totalSize }) => { }; /** - * Renders layout to display user's most frequently used programming languages. + * Create donut languages text items for all programming languages. + * + * @param {object} props Function properties. + * @param {Lang[]} props.langs Array of programming languages. + * @param {number} props.totalSize Total size of all languages. + * @returns {string} Donut layout programming language SVG node. + */ +const createDonutLanguagesNode = ({ langs, totalSize }) => { + return flexLayout({ + items: langs.map((lang, index) => { + return createCompactLangNode({ + lang, + totalSize, + hideProgress: false, + index, + }); + }), + gap: 32, + direction: "column", + }).join(""); +}; + +/** + * Renders the default language card layout. * * @param {Lang[]} langs Array of programming languages. * @param {number} width Card width. @@ -155,14 +333,15 @@ const renderNormalLayout = (langs, width, totalLanguageSize) => { }; /** - * Renders compact layout to display user's most frequently used programming languages. + * Renders the compact language card layout. * * @param {Lang[]} langs Array of programming languages. * @param {number} width Card width. * @param {number} totalLanguageSize Total size of all languages. + * @param {boolean} hideProgress Whether to hide progress bar. * @returns {string} Compact layout card SVG object. */ -const renderCompactLayout = (langs, width, totalLanguageSize) => { +const renderCompactLayout = (langs, width, totalLanguageSize, hideProgress) => { const paddingRight = 50; const offsetWidth = width - paddingRight; // progressOffset holds the previous language's width and used to offset the next language @@ -193,75 +372,304 @@ const renderCompactLayout = (langs, width, totalLanguageSize) => { .join(""); return ` - + ${ + !hideProgress + ? ` + ${compactProgressBar} - - + ` + : "" + } + ${createLanguageTextNode({ langs, totalSize: totalLanguageSize, + hideProgress: hideProgress, })} `; }; /** - * Calculates height for the compact layout. + * Renders donut vertical layout to display user's most frequently used programming languages. * - * @param {number} totalLangs Total number of languages. - * @returns {number} Card height. + * @param {Lang[]} langs Array of programming languages. + * @param {number} totalLanguageSize Total size of all languages. + * @returns {string} Compact layout card SVG object. */ -const calculateCompactLayoutHeight = (totalLangs) => { - return 90 + Math.round(totalLangs / 2) * 25; +const renderDonutVerticalLayout = (langs, totalLanguageSize) => { + // Donut vertical chart radius and total length + const radius = 80; + const totalCircleLength = getCircleLength(radius); + + // SVG circles + let circles = []; + + // Start indent for donut vertical chart parts + let indent = 0; + + // Start delay coefficient for donut vertical chart parts + let startDelayCoefficient = 1; + + // Generate each donut vertical chart part + for (const lang of langs) { + const percentage = (lang.size / totalLanguageSize) * 100; + const circleLength = totalCircleLength * (percentage / 100); + const delay = startDelayCoefficient * 100; + + circles.push(` + + + + `); + + // Update the indent for the next part + indent += circleLength; + // Update the start delay coefficient for the next part + startDelayCoefficient += 1; + } + + return ` + + + + ${circles.join("")} + + + + + ${createLanguageTextNode({ + langs, + totalSize: totalLanguageSize, + hideProgress: false, + })} + + + + `; }; /** - * Calculates height for the normal layout. + * Renders pie layout to display user's most frequently used programming languages. * - * @param {number} totalLangs Total number of languages. - * @returns {number} Card height. + * @param {Lang[]} langs Array of programming languages. + * @param {number} totalLanguageSize Total size of all languages. + * @returns {string} Compact layout card SVG object. */ -const calculateNormalLayoutHeight = (totalLangs) => { - return 45 + (totalLangs + 1) * 40; +const renderPieLayout = (langs, totalLanguageSize) => { + // Pie chart radius and center coordinates + const radius = 90; + const centerX = 150; + const centerY = 100; + + // Start angle for the pie chart parts + let startAngle = 0; + + // Start delay coefficient for the pie chart parts + let startDelayCoefficient = 1; + + // SVG paths + const paths = []; + + // Generate each pie chart part + for (const lang of langs) { + if (langs.length === 1) { + paths.push(` + + `); + break; + } + + const langSizePart = lang.size / totalLanguageSize; + const percentage = langSizePart * 100; + // Calculate the angle for the current part + const angle = langSizePart * 360; + + // Calculate the end angle + const endAngle = startAngle + angle; + + // Calculate the coordinates of the start and end points of the arc + const startPoint = polarToCartesian(centerX, centerY, radius, startAngle); + const endPoint = polarToCartesian(centerX, centerY, radius, endAngle); + + // Determine the large arc flag based on the angle + const largeArcFlag = angle > 180 ? 1 : 0; + + // Calculate delay + const delay = startDelayCoefficient * 100; + + // SVG arc markup + paths.push(` + + + + `); + + // Update the start angle for the next part + startAngle = endAngle; + // Update the start delay coefficient for the next part + startDelayCoefficient += 1; + } + + return ` + + + + ${paths.join("")} + + + + + ${createLanguageTextNode({ + langs, + totalSize: totalLanguageSize, + hideProgress: false, + })} + + + + `; }; /** - * Hides languages and trims the list to show only the top N languages. + * Creates the SVG paths for the language donut chart. * - * @param {Record} topLangs Top languages. - * @param {string[]} hide Languages to hide. - * @param {string} langs_count Number of languages to show. + * @param {number} cx Donut center x-position. + * @param {number} cy Donut center y-position. + * @param {number} radius Donut arc Radius. + * @param {number[]} percentages Array with donut section percentages. + * @returns {{d: string, percent: number}[]} Array of svg path elements */ -const useLanguages = (topLangs, hide, langs_count) => { - let langs = Object.values(topLangs); - let langsToHide = {}; - let langsCount = clampValue(parseInt(langs_count), 1, 10); +const createDonutPaths = (cx, cy, radius, percentages) => { + const paths = []; + let startAngle = 0; + let endAngle = 0; - // populate langsToHide map for quick lookup - // while filtering out - if (hide) { - hide.forEach((langName) => { - langsToHide[lowercaseTrim(langName)] = true; - }); + const totalPercent = percentages.reduce((acc, curr) => acc + curr, 0); + for (let i = 0; i < percentages.length; i++) { + const tmpPath = {}; + + let percent = parseFloat( + ((percentages[i] / totalPercent) * 100).toFixed(2), + ); + + endAngle = 3.6 * percent + startAngle; + const startPoint = polarToCartesian(cx, cy, radius, endAngle - 90); // rotate donut 90 degrees counter-clockwise. + const endPoint = polarToCartesian(cx, cy, radius, startAngle - 90); // rotate donut 90 degrees counter-clockwise. + const largeArc = endAngle - startAngle <= 180 ? 0 : 1; + + tmpPath.percent = percent; + tmpPath.d = `M ${startPoint.x} ${startPoint.y} A ${radius} ${radius} 0 ${largeArc} 0 ${endPoint.x} ${endPoint.y}`; + + paths.push(tmpPath); + startAngle = endAngle; } - // filter out languages to be hidden - langs = langs - .sort((a, b) => b.size - a.size) - .filter((lang) => { - return !langsToHide[lowercaseTrim(lang.name)]; - }) - .slice(0, langsCount); + return paths; +}; - const totalLanguageSize = langs.reduce((acc, curr) => acc + curr.size, 0); +/** + * Renders the donut language card layout. + * + * @param {Lang[]} langs Array of programming languages. + * @param {number} width Card width. + * @param {number} totalLanguageSize Total size of all languages. + * @returns {string} Donut layout card SVG object. + */ +const renderDonutLayout = (langs, width, totalLanguageSize) => { + const centerX = width / 3; + const centerY = width / 3; + const radius = centerX - 60; + const strokeWidth = 12; + + const colors = langs.map((lang) => lang.color); + const langsPercents = langs.map((lang) => + parseFloat(((lang.size / totalLanguageSize) * 100).toFixed(2)), + ); - return { langs, totalLanguageSize }; + const langPaths = createDonutPaths(centerX, centerY, radius, langsPercents); + + const donutPaths = + langs.length === 1 + ? `` + : langPaths + .map((section, index) => { + const staggerDelay = (index + 3) * 100; + const delay = staggerDelay + 300; + + const output = ` + + + + + `; + + return output; + }) + .join(""); + + const donut = `${donutPaths}`; + + return ` + + + ${createDonutLanguagesNode({ langs, totalSize: totalLanguageSize })} + + + + ${donut} + + + `; +}; + +/** + * Creates the no coding activity SVG node. + * + * @param {{color: string, text: string, layout: import("./types").TopLangOptions["layout"]}} The function prop + */ +const noLanguagesDataNode = ({ color, text, layout }) => { + return ` + ${text} + `; }; /** - * Renders card to display user's most frequently used programming languages. + * Renders card that display user's most frequently used programming languages. * * @param {import('../fetchers/types').TopLangData} topLangs User's most frequently used programming languages. * @param {Partial} options Card options. @@ -276,6 +684,7 @@ const renderTopLanguages = (topLangs, options = {}) => { text_color, bg_color, hide, + hide_progress, theme, layout, custom_title, @@ -291,7 +700,7 @@ const renderTopLanguages = (topLangs, options = {}) => { translations: langCardLocales, }); - const { langs, totalLanguageSize } = useLanguages( + const { langs, totalLanguageSize } = trimTopLanguages( topLangs, hide, String(langs_count), @@ -304,16 +713,6 @@ const renderTopLanguages = (topLangs, options = {}) => { : card_width; let height = calculateNormalLayoutHeight(langs.length); - let finalLayout = ""; - if (layout === "compact") { - width = width + 50; // padding - height = calculateCompactLayoutHeight(langs.length); - - finalLayout = renderCompactLayout(langs, width, totalLanguageSize); - } else { - finalLayout = renderNormalLayout(langs, width, totalLanguageSize); - } - // returns theme based colors with proper overrides and defaults const colors = getCardColors({ title_color, @@ -323,6 +722,38 @@ const renderTopLanguages = (topLangs, options = {}) => { theme, }); + let finalLayout = ""; + if (langs.length === 0) { + height = COMPACT_LAYOUT_BASE_HEIGHT; + finalLayout = noLanguagesDataNode({ + color: colors.textColor, + text: i18n.t("langcard.nodata"), + layout, + }); + } else if (layout === "pie") { + height = calculatePieLayoutHeight(langs.length); + finalLayout = renderPieLayout(langs, totalLanguageSize); + } else if (layout === "donut-vertical") { + height = calculateDonutVerticalLayoutHeight(langs.length); + finalLayout = renderDonutVerticalLayout(langs, totalLanguageSize); + } else if (layout === "compact" || hide_progress == true) { + height = + calculateCompactLayoutHeight(langs.length) + (hide_progress ? -25 : 0); + + finalLayout = renderCompactLayout( + langs, + width, + totalLanguageSize, + hide_progress, + ); + } else if (layout?.toLowerCase() === "donut") { + height = calculateDonutLayoutHeight(langs.length); + width = width + 50; // padding + finalLayout = renderDonutLayout(langs, width, totalLanguageSize); + } else { + finalLayout = renderNormalLayout(langs, width, totalLanguageSize); + } + const card = new Card({ customTitle: custom_title, defaultTitle: i18n.t("langcard.title"), @@ -354,6 +785,14 @@ const renderTopLanguages = (topLangs, options = {}) => { width: 100%; } } + .stat { + font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${colors.textColor}; + } + @supports(-moz-appearance: auto) { + /* Selector detects Firefox */ + .stat { font-size:12px; } + } + .bold { font-weight: 700 } .lang-name { font: 400 11px "Segoe UI", Ubuntu, Sans-Serif; fill: ${colors.textColor}; @@ -371,6 +810,10 @@ const renderTopLanguages = (topLangs, options = {}) => { `, ); + if (layout === "pie" || layout === "donut-vertical") { + return card.render(finalLayout); + } + return card.render(` ${finalLayout} @@ -378,4 +821,20 @@ const renderTopLanguages = (topLangs, options = {}) => { `); }; -export { renderTopLanguages, MIN_CARD_WIDTH }; +export { + getLongestLang, + degreesToRadians, + radiansToDegrees, + polarToCartesian, + cartesianToPolar, + getCircleLength, + calculateCompactLayoutHeight, + calculateNormalLayoutHeight, + calculateDonutLayoutHeight, + calculateDonutVerticalLayoutHeight, + calculatePieLayoutHeight, + donutCenterTranslation, + trimTopLanguages, + renderTopLanguages, + MIN_CARD_WIDTH, +}; diff --git a/src/cards/types.d.ts b/src/cards/types.d.ts index c5945d48be71e4..d6a1de05d176f6 100644 --- a/src/cards/types.d.ts +++ b/src/cards/types.d.ts @@ -1,4 +1,5 @@ type ThemeNames = keyof typeof import("../../themes/index.js"); +type RankIcon = "default" | "github"; export type CommonOptions = { title_color: string; @@ -22,6 +23,10 @@ export type StatCardOptions = CommonOptions & { line_height: number | string; custom_title: string; disable_animations: boolean; + number_format: string; + ring_color: string; + text_bold: boolean; + rank_icon: RankIcon; }; export type RepoCardOptions = CommonOptions & { @@ -34,10 +39,11 @@ export type TopLangOptions = CommonOptions & { hide_border: boolean; card_width: number; hide: string[]; - layout: "compact" | "normal"; + layout: "compact" | "normal" | "donut" | "donut-vertical" | "pie"; custom_title: string; langs_count: number; disable_animations: boolean; + hide_progress: boolean; }; type WakaTimeOptions = CommonOptions & { diff --git a/src/cards/wakatime-card.js b/src/cards/wakatime-card.js index e7af1df710f9cd..9c17a4c063db7b 100644 --- a/src/cards/wakatime-card.js +++ b/src/cards/wakatime-card.js @@ -36,11 +36,11 @@ const noCodingActivityNode = ({ color, text }) => { /** * Create compact WakaTime layout. * - * @param {Object[]} args The function arguments. - * @param {import("../fetchers/types").WakaTimeLang[]} languages The languages array. - * @param {number} totalSize The total size of the languages. - * @param {number} x The x position of the language node. - * @param {number} y The y position of the language node. + * @param {Object} args The function arguments. + * @param {import("../fetchers/types").WakaTimeLang} args.lang The languages array. + * @param {number} args.totalSize The total size of the languages. + * @param {number} args.x The x position of the language node. + * @param {number} args.y The y position of the language node. */ const createCompactLangNode = ({ lang, totalSize, x, y }) => { const color = languageColors[lang.name] || "#858585"; @@ -58,11 +58,11 @@ const createCompactLangNode = ({ lang, totalSize, x, y }) => { /** * Create WakaTime language text node item. * - * @param {Object[]} args The function arguments. - * @param {import("../fetchers/types").WakaTimeLang} lang The language object. - * @param {number} totalSize The total size of the languages. - * @param {number} x The x position of the language node. - * @param {number} y The y position of the language node. + * @param {Object} args The function arguments. + * @param {import("../fetchers/types").WakaTimeLang[]} args.langs The language objects. + * @param {number} args.totalSize The total size of the languages. + * @param {number} args.x The x position of the language node. + * @param {number} args.y The y position of the language node. */ const createLanguageTextNode = ({ langs, totalSize, x, y }) => { return langs.map((lang, index) => { @@ -86,14 +86,15 @@ const createLanguageTextNode = ({ langs, totalSize, x, y }) => { /** * Create WakaTime text item. * - * @param {Object[]} args The function arguments. - * @param {string} id The id of the text node item. - * @param {string} label The label of the text node item. - * @param {string} value The value of the text node item. - * @param {number} index The index of the text node item. - * @param {percent} percent Percentage of the text node item. - * @param {boolean} hideProgress Whether to hide the progress bar. - * @param {string} progressBarBackgroundColor The color of the progress bar background. + * @param {Object} args The function arguments. + * @param {string} args.id The id of the text node item. + * @param {string} args.label The label of the text node item. + * @param {string} args.value The value of the text node item. + * @param {number} args.index The index of the text node item. + * @param {string} args.percent Percentage of the text node item. + * @param {boolean} args.hideProgress Whether to hide the progress bar. + * @param {string} args.progressBarColor The color of the progress bar. + * @param {string} args.progressBarBackgroundColor The color of the progress bar background. */ const createTextNode = ({ id, @@ -118,6 +119,7 @@ const createTextNode = ({ // @ts-ignore name: label, progressBarBackgroundColor, + delay: staggerDelay + 300, }); return ` @@ -138,7 +140,7 @@ const createTextNode = ({ * hiding languages. * * @param {import("../fetchers/types").WakaTimeLang[]} languages The languages array. - * @return {import("../fetchers/types").WakaTimeLang[]} The recalculated languages array. + * @return {void} The recalculated languages array. */ const recalculatePercentages = (languages) => { const totalSum = languages.reduce( @@ -266,21 +268,34 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { ${compactProgressBar} - ${createLanguageTextNode({ - x: 0, - y: 25, - langs: filteredLanguages, - totalSize: 100, - }).join("")} + ${ + filteredLanguages.length + ? createLanguageTextNode({ + x: 0, + y: 25, + langs: filteredLanguages, + totalSize: 100, + }).join("") + : noCodingActivityNode({ + // @ts-ignore + color: textColor, + text: !stats.is_coding_activity_visible + ? i18n.t("wakatimecard.notpublic") + : stats.is_other_usage_visible + ? i18n.t("wakatimecard.nocodingactivity") + : i18n.t("wakatimecard.nocodedetails"), + }) + } `; } else { finalLayout = flexLayout({ items: filteredLanguages.length - ? filteredLanguages.map((language) => { + ? filteredLanguages.map((language, index) => { return createTextNode({ id: language.name, label: language.name, value: language.text, + index: index, percent: language.percent, // @ts-ignore progressBarColor: titleColor, @@ -293,7 +308,11 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { noCodingActivityNode({ // @ts-ignore color: textColor, - text: i18n.t("wakatimecard.nocodingactivity"), + text: !stats.is_coding_activity_visible + ? i18n.t("wakatimecard.notpublic") + : stats.is_other_usage_visible + ? i18n.t("wakatimecard.nocodingactivity") + : i18n.t("wakatimecard.nocodedetails"), }), ], gap: lheight, @@ -301,9 +320,20 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { }).join(""); } + // Get title range text + let titleText = i18n.t("wakatimecard.title"); + switch (stats.range) { + case "last_7_days": + titleText += ` (${i18n.t("wakatimecard.last7days")})`; + break; + case "last_year": + titleText += ` (${i18n.t("wakatimecard.lastyear")})`; + break; + } + const card = new Card({ customTitle: custom_title, - defaultTitle: i18n.t("wakatimecard.title"), + defaultTitle: titleText, width: 495, height, border_radius, @@ -321,7 +351,29 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { card.setCSS( ` ${cssStyles} + @keyframes slideInAnimation { + from { + width: 0; + } + to { + width: calc(100%-100px); + } + } + @keyframes growWidthAnimation { + from { + width: 0; + } + to { + width: 100%; + } + } .lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} } + #rect-mask rect{ + animation: slideInAnimation 1s ease-in-out forwards; + } + .lang-progress{ + animation: growWidthAnimation 0.6s ease-in-out forwards; + } `, ); diff --git a/src/common/Card.js b/src/common/Card.js index 2f1d9c29f274de..c61b17db73a2b5 100644 --- a/src/common/Card.js +++ b/src/common/Card.js @@ -12,6 +12,12 @@ class Card { * @param {string?=} args.customTitle Card custom title. * @param {string?=} args.defaultTitle Card default title. * @param {string?=} args.titlePrefixIcon Card title prefix icon. + * @param {object?=} args.colors Card colors arguments. + * @param {string} args.colors.titleColor Card title color. + * @param {string} args.colors.textColor Card text color. + * @param {string} args.colors.iconColor Card icon color. + * @param {string|Array} args.colors.bgColor Card background color. + * @param {string} args.colors.borderColor Card border color. * @returns {Card} Card instance. */ constructor({ diff --git a/src/common/icons.js b/src/common/icons.js index 5282a93ec8725e..948ca0bc427d15 100644 --- a/src/common/icons.js +++ b/src/common/icons.js @@ -8,5 +8,28 @@ const icons = { fork: ``, }; -export { icons }; +/** + * Get rank icon + * + * @returns {string} - The SVG code of the rank icon + */ +const rankIcon = (rankIcon, rankLevel) => { + switch (rankIcon) { + case "github": + return ` + + `; + case "default": + default: + return ` + + ${rankLevel} + + `; + } +}; + +export { icons, rankIcon }; export default icons; diff --git a/src/common/languageColors.json b/src/common/languageColors.json index 7e8cd551264b8a..b670823cfd8635 100644 --- a/src/common/languageColors.json +++ b/src/common/languageColors.json @@ -79,6 +79,7 @@ "Ceylon": "#dfa535", "Chapel": "#8dc63f", "ChucK": "#3f8000", + "Circom": "#707575", "Cirru": "#ccccff", "Clarion": "#db901e", "Clarity": "#5546ff", @@ -102,8 +103,10 @@ "Csound Score": "#1a1a1a", "Cuda": "#3A4E3A", "Curry": "#531242", + "Cypher": "#34c0eb", "Cython": "#fedf5b", "D": "#ba595e", + "D2": "#526ee8", "DM": "#447265", "Dafny": "#FFEC25", "Darcs Patch": "#8eff23", @@ -115,6 +118,7 @@ "DirectX 3D File": "#aace60", "Dockerfile": "#384d54", "Dogescript": "#cca760", + "Dotenv": "#e5d559", "Dylan": "#6c616e", "E": "#ccce35", "ECL": "#8a1267", @@ -124,11 +128,13 @@ "Earthly": "#2af0ff", "Easybuild": "#069406", "Ecere Projects": "#913960", + "Ecmarkup": "#eb8131", "EditorConfig": "#fff1f2", "Eiffel": "#4d6977", "Elixir": "#6e4a7e", "Elm": "#60B5CC", "Elvish": "#55BB55", + "Elvish Transcript": "#55BB55", "Emacs Lisp": "#c065db", "EmberScript": "#FFF4F3", "Erlang": "#B83998", @@ -147,7 +153,7 @@ "Forth": "#341708", "Fortran": "#4d41b1", "Fortran Free Form": "#4d41b1", - "FreeBasic": "#867db1", + "FreeBasic": "#141AC9", "FreeMarker": "#0050b2", "Frege": "#00cafe", "Futhark": "#5f021f", @@ -180,6 +186,8 @@ "Go": "#00ADD8", "Go Checksums": "#00ADD8", "Go Module": "#00ADD8", + "Go Workspace": "#00ADD8", + "Godot Resource": "#355570", "Golo": "#88562A", "Gosu": "#82937f", "Grace": "#615f8b", @@ -190,6 +198,7 @@ "Groovy": "#4298b8", "Groovy Server Pages": "#4298b8", "HAProxy": "#106da9", + "HCL": "#844FBA", "HLSL": "#aace60", "HOCON": "#9ff8ee", "HTML": "#e34c26", @@ -215,6 +224,7 @@ "Idris": "#b30000", "Ignore List": "#000000", "ImageJ Macro": "#99AAFF", + "Imba": "#16cec6", "Inno Setup": "#264b99", "Io": "#a9188d", "Ioke": "#078193", @@ -222,6 +232,7 @@ "Isabelle ROOT": "#FEFE00", "J": "#9EEDFF", "JAR Manifest": "#b07219", + "JCL": "#d90e09", "JFlex": "#DBCA00", "JSON": "#292929", "JSON with Comments": "#292929", @@ -244,9 +255,11 @@ "Jsonnet": "#0064bd", "Julia": "#a270ba", "Jupyter Notebook": "#DA5B0B", + "Just": "#384d54", "KRL": "#28430A", "Kaitai Struct": "#773b37", "KakouneScript": "#6f8042", + "KerboScript": "#41adf0", "KiCad Layout": "#2f4aab", "KiCad Legacy Layout": "#2f4aab", "KiCad Schematic": "#2f4aab", @@ -273,6 +286,7 @@ "Lua": "#000080", "MATLAB": "#e16737", "MAXScript": "#00a6a6", + "MDX": "#fcb32c", "MLIR": "#5EC8DB", "MQL4": "#62A8D6", "MQL5": "#4A76B8", @@ -286,6 +300,7 @@ "Mathematica": "#dd1100", "Max": "#c4a79c", "Mercury": "#ff2b2b", + "Mermaid": "#ff3670", "Meson": "#007800", "Metal": "#8f14e9", "MiniYAML": "#ff1111", @@ -318,7 +333,12 @@ "Nu": "#c9df40", "NumPy": "#9C8AF9", "Nunjucks": "#3d8137", - "OCaml": "#3be133", + "Nushell": "#4E9906", + "OASv2-json": "#85ea2d", + "OASv2-yaml": "#85ea2d", + "OASv3-json": "#85ea2d", + "OASv3-yaml": "#85ea2d", + "OCaml": "#ef7a08", "ObjectScript": "#424893", "Objective-C": "#438eff", "Objective-C++": "#6866fb", @@ -327,19 +347,24 @@ "Omgrofl": "#cabbff", "Opal": "#f7ede0", "Open Policy Agent": "#7d9199", + "OpenAPI Specification v2": "#85ea2d", + "OpenAPI Specification v3": "#85ea2d", "OpenCL": "#ed2e2d", "OpenEdge ABL": "#5ce600", "OpenQASM": "#AA70FF", "OpenSCAD": "#e5cd45", + "Option List": "#476732", "Org": "#77aa99", "Oxygene": "#cdd0e3", "Oz": "#fab738", "P4": "#7055b5", + "PDDL": "#0d00ff", "PEG.js": "#234d6b", "PHP": "#4F5D95", "PLSQL": "#dad8d8", "PLpgSQL": "#336790", "POV-Ray SDL": "#6bac65", + "Pact": "#F7A8B8", "Pan": "#cc0000", "Papyrus": "#6600cc", "Parrot": "#f3ca0a", @@ -350,7 +375,9 @@ "PicoLisp": "#6067af", "PigLatin": "#fcd7de", "Pike": "#005390", + "PlantUML": "#fbbd16", "PogoScript": "#d80074", + "Polar": "#ae81ff", "Portugol": "#f8bd00", "PostCSS": "#dc3a0c", "PostScript": "#da291c", @@ -366,6 +393,7 @@ "Puppet": "#302B6D", "PureBasic": "#5a6986", "PureScript": "#1D222D", + "Pyret": "#ee1e10", "Python": "#3572A5", "Python console": "#3572A5", "Python traceback": "#3572A5", @@ -375,6 +403,7 @@ "Quake": "#882233", "R": "#198CE7", "RAML": "#77d9fb", + "RBS": "#701516", "RDoc": "#701516", "REXX": "#d90e09", "RMarkdown": "#198ce7", @@ -414,6 +443,7 @@ "Sass": "#a53b70", "Scala": "#c22d40", "Scaml": "#bd181a", + "Scenic": "#fdc700", "Scheme": "#1e4aec", "Scilab": "#ca0f21", "Self": "#0579aa", @@ -421,6 +451,7 @@ "Shell": "#89e051", "ShellCheck Config": "#cecfcb", "Shen": "#120F14", + "Simple File Verification": "#C9BFED", "Singularity": "#64E6AD", "Slash": "#007eff", "Slice": "#003fa2", @@ -428,6 +459,8 @@ "SmPL": "#c94949", "Smalltalk": "#596706", "Smarty": "#f0c040", + "Smithy": "#c44536", + "Snakemake": "#419179", "Solidity": "#AA6746", "SourcePawn": "#f69e1d", "Squirrel": "#800000", @@ -441,9 +474,11 @@ "SugarSS": "#2fcc9f", "SuperCollider": "#46390b", "Svelte": "#ff3e00", + "Sway": "#dea584", "Swift": "#F05138", "SystemVerilog": "#DAE1C2", "TI Program": "#A0AA87", + "TL-Verilog": "#C40023", "TLA": "#4b0079", "TOML": "#9c4221", "TSQL": "#e38c00", @@ -478,11 +513,13 @@ "Vim Script": "#199f4b", "Vim Snippet": "#199f4b", "Visual Basic .NET": "#945db7", + "Visual Basic 6.0": "#2c6353", "Volt": "#1F1F1F", "Vue": "#41b883", "Vyper": "#2980b9", "Web Ontology Language": "#5b70bd", "WebAssembly": "#04133b", + "WebAssembly Interface Type": "#6250e7", "Whiley": "#d5c397", "Wikitext": "#fc5757", "Windows Registry Entries": "#52d5ff", diff --git a/src/common/utils.js b/src/common/utils.js index 1215fc9ac8cc21..d58203be69508d 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -194,15 +194,15 @@ const flexLayout = ({ items, gap, direction, sizes = [] }) => { /** * Returns theme based colors with proper overrides and defaults. * - * @param {Object[]} args Function arguments. + * @param {Object} args Function arguments. * @param {string} args.title_color Card title color. * @param {string} args.text_color Card text color. * @param {string} args.icon_color Card icon color. * @param {string} args.bg_color Card background color. * @param {string} args.border_color Card border color. + * @param {string} args.ring_color Card ring color. * @param {string} args.theme Card theme. * @param {string} args.fallbackTheme Fallback theme. - * */ const getCardColors = ({ title_color, @@ -305,6 +305,7 @@ const SECONDARY_ERROR_MESSAGES = { "Please add an env variable called PAT_1 with your github token in vercel", USER_NOT_FOUND: "Make sure the provided username is not an organization", GRAPHQL_ERROR: "Please try again later", + WAKATIME_USER_NOT_FOUND: "Make sure you have a public WakaTime profile", }; /** @@ -324,6 +325,7 @@ class CustomError extends Error { static MAX_RETRY = "MAX_RETRY"; static USER_NOT_FOUND = "USER_NOT_FOUND"; static GRAPHQL_ERROR = "GRAPHQL_ERROR"; + static WAKATIME_ERROR = "WAKATIME_ERROR"; } /** @@ -392,7 +394,7 @@ const lowercaseTrim = (name) => name.toLowerCase().trim(); /** * Split array of languages in two columns. * - * @template T Langauge object. + * @template T Language object. * @param {Array} arr Array of languages. * @param {number} perChunk Number of languages per column. * @returns {Array} Array of languages split in two columns. @@ -424,6 +426,19 @@ const parseEmojis = (str) => { }); }; +/** + * Get diff in minutes + * @param {Date} d1 + * @param {Date} d2 + * @returns {number} + */ +const dateDiff = (d1, d2) => { + const date1 = new Date(d1); + const date2 = new Date(d2); + const diff = date1.getTime() - date2.getTime(); + return Math.round(diff / (1000 * 60)); +}; + export { ERROR_CARD_LENGTH, renderError, @@ -447,4 +462,5 @@ export { lowercaseTrim, chunkArray, parseEmojis, + dateDiff, }; diff --git a/src/fetchers/repo-fetcher.js b/src/fetchers/repo-fetcher.js index ff7a2be8164cc7..412d1d4c28cf18 100644 --- a/src/fetchers/repo-fetcher.js +++ b/src/fetchers/repo-fetcher.js @@ -5,9 +5,9 @@ import { MissingParamError, request } from "../common/utils.js"; /** * Repo data fetcher. * - * @param {import('Axios').AxiosRequestHeaders} variables Fetcher variables. + * @param {import('axios').AxiosRequestHeaders} variables Fetcher variables. * @param {string} token GitHub token. - * @returns {Promise} The response. + * @returns {Promise} The response. */ const fetcher = (variables, token) => { return request( diff --git a/src/fetchers/stats-fetcher.js b/src/fetchers/stats-fetcher.js index a7df1e504db2f8..8fecffa466f8d7 100644 --- a/src/fetchers/stats-fetcher.js +++ b/src/fetchers/stats-fetcher.js @@ -192,7 +192,7 @@ const fetchStats = async ( totalIssues: 0, totalStars: 0, contributedTo: 0, - rank: { level: "C", score: 0 }, + rank: { level: "B", score: 0 }, }; let res = await statsFetcher(username); @@ -213,61 +213,51 @@ const fetchStats = async ( ); } throw new CustomError( - "Something went while trying to retrieve the stats data using the GraphQL API.", + "Something went wrong while trying to retrieve the stats data using the GraphQL API.", CustomError.GRAPHQL_ERROR, ); } 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.openIssues.totalCount + user.closedIssues.totalCount; - - // normal commits - stats.totalCommits = user.contributionsCollection.totalCommitContributions; - // if include_all_commits then just get that, - // since totalCommitsFetcher already sends totalCommits no need to += + // if include_all_commits, fetch all commits using the REST API. if (include_all_commits) { stats.totalCommits = await totalCommitsFetcher(username); + } else { + stats.totalCommits = user.contributionsCollection.totalCommitContributions; } - // if count_private then add private commits to totalCommits so far. + // if count_private, add private contributions to totalCommits. if (count_private) { stats.totalCommits += user.contributionsCollection.restrictedContributionsCount; } stats.totalPRs = user.pullRequests.totalCount; + stats.totalIssues = user.openIssues.totalCount + user.closedIssues.totalCount; stats.contributedTo = user.repositoriesContributedTo.totalCount; - // Retrieve stars while filtering out repositories to be hidden + // Retrieve stars while filtering out repositories to be hidden. + let repoToHide = new Set(exclude_repo); + stats.totalStars = user.repositories.nodes .filter((data) => { - return !repoToHide[data.name]; + return !repoToHide.has(data.name); }) .reduce((prev, curr) => { return prev + curr.stargazers.totalCount; }, 0); - // @ts-ignore // TODO: Fix this. stats.rank = calculateRank({ - totalCommits: stats.totalCommits, - totalRepos: user.repositories.totalCount, - followers: user.followers.totalCount, - contributions: stats.contributedTo, - stargazers: stats.totalStars, + all_commits: include_all_commits, + commits: stats.totalCommits, prs: stats.totalPRs, issues: stats.totalIssues, + repos: user.repositories.totalCount, + stars: stats.totalStars, + followers: user.followers.totalCount, }); return stats; diff --git a/src/fetchers/top-languages-fetcher.js b/src/fetchers/top-languages-fetcher.js index 86d794435be088..b57d901afb0a77 100644 --- a/src/fetchers/top-languages-fetcher.js +++ b/src/fetchers/top-languages-fetcher.js @@ -11,7 +11,7 @@ import { /** * Top languages fetcher object. * - * @param {import('Axios').AxiosRequestHeaders} variables Fetcher variables. + * @param {import('axios').AxiosRequestHeaders} variables Fetcher variables. * @param {string} token GitHub token. * @returns {Promise} Languages fetcher response. */ @@ -54,7 +54,12 @@ const fetcher = (variables, token) => { * @param {string[]} exclude_repo List of repositories to exclude. * @returns {Promise} Top languages data. */ -const fetchTopLanguages = async (username, exclude_repo = []) => { +const fetchTopLanguages = async ( + username, + exclude_repo = [], + size_weight = 1, + count_weight = 0, +) => { if (!username) throw new MissingParamError(["username"]); const res = await retryer(fetcher, { login: username }); @@ -101,6 +106,8 @@ const fetchTopLanguages = async (username, exclude_repo = []) => { .sort((a, b) => b.size - a.size) .filter((name) => !repoToHide[name.name]); + let repoCount = 0; + repoNodes = repoNodes .filter((node) => node.languages.edges.length > 0) // flatten the list of language nodes @@ -111,9 +118,14 @@ const fetchTopLanguages = async (username, exclude_repo = []) => { // if we already have the language in the accumulator // & the current language name is same as previous name - // add the size to the language size. + // add the size to the language size and increase repoCount. if (acc[prev.node.name] && prev.node.name === acc[prev.node.name].name) { langSize = prev.size + acc[prev.node.name].size; + repoCount += 1; + } else { + // reset repoCount to 1 + // language must exist in at least one repo to be detected + repoCount = 1; } return { ...acc, @@ -121,10 +133,18 @@ const fetchTopLanguages = async (username, exclude_repo = []) => { name: prev.node.name, color: prev.node.color, size: langSize, + count: repoCount, }, }; }, {}); + Object.keys(repoNodes).forEach((name) => { + // comparison index calculation + repoNodes[name].size = + Math.pow(repoNodes[name].size, size_weight) * + Math.pow(repoNodes[name].count, count_weight); + }); + const topLangs = Object.keys(repoNodes) .sort((a, b) => repoNodes[b].size - repoNodes[a].size) .reduce((result, key) => { diff --git a/src/fetchers/wakatime-fetcher.js b/src/fetchers/wakatime-fetcher.js index fa1f3d890920ff..4578b9eb0ddda1 100644 --- a/src/fetchers/wakatime-fetcher.js +++ b/src/fetchers/wakatime-fetcher.js @@ -1,27 +1,29 @@ import axios from "axios"; -import { MissingParamError } from "../common/utils.js"; +import { CustomError, MissingParamError } from "../common/utils.js"; +import { I18n } from "../common/I18n.js"; /** * WakaTime data fetcher. * - * @param {{username: string, api_domain: string, range: string}} props Fetcher props. + * @param {{username: string, api_domain: string }} props Fetcher props. * @returns {Promise} WakaTime data response. */ -const fetchWakatimeStats = async ({ username, api_domain, range }) => { +const fetchWakatimeStats = async ({ username, api_domain }) => { if (!username) throw new MissingParamError(["username"]); try { const { data } = await axios.get( `https://${ api_domain ? api_domain.replace(/\/$/gi, "") : "wakatime.com" - }/api/v1/users/${username}/stats/${range || ""}?is_including_today=true`, + }/api/v1/users/${username}/stats?is_including_today=true`, ); return data.data; } catch (err) { if (err.response.status < 200 || err.response.status > 299) { - throw new Error( - "Wakatime user not found, make sure you have a wakatime profile", + throw new CustomError( + `Could not resolve to a User with the login of '${username}'`, + "WAKATIME_USER_NOT_FOUND", ); } throw err; diff --git a/src/getStyles.js b/src/getStyles.js index f7b90f4adc7b4c..7e6113921b60db 100644 --- a/src/getStyles.js +++ b/src/getStyles.js @@ -65,10 +65,11 @@ const getAnimations = () => { /** * Retrieves CSS styles for a card. * - * @param {Object[]} colors The colors to use for the card. + * @param {Object} colors The colors to use for the card. * @param {string} colors.titleColor The title color. * @param {string} colors.textColor The text color. * @param {string} colors.iconColor The icon color. + * @param {string} colors.ringColor The ring color. * @param {boolean} colors.show_icons Whether to show icons. * @param {number} colors.progress The progress value to animate to. * @returns {string} Card CSS styles. diff --git a/src/translations.js b/src/translations.js index 45c8295e024de6..c123eb346f0bd2 100644 --- a/src/translations.js +++ b/src/translations.js @@ -283,7 +283,7 @@ const langCardLocales = { np: "अधिक प्रयोग गरिएको भाषाहरू", el: "Οι περισσότερο χρησιμοποιούμενες γλώσσες", ru: "Наиболее часто используемые языки", - "uk-ua": "Найбільш часто використовувані мови", + "uk-ua": "Найчастіше використовувані мови", id: "Bahasa Yang Paling Banyak Digunakan", ml: "കൂടുതൽ ഉപയോഗിച്ച ഭാഷകൾ", my: "Bahasa Paling Digunakan", @@ -293,6 +293,36 @@ const langCardLocales = { vi: "Ngôn Ngữ Thường Sử Dụng", se: "Mest använda språken", }, + "langcard.nodata": { + ar: "لا توجد بيانات لغات.", + cn: "沒有語言數據。", + "zh-tw": "沒有語言數據。", + cs: "Žádné jazykové údaje.", + de: "Keine Sprachdaten.", + bn: "কোন ভাষার ডেটা নেই।", + en: "No languages data.", + es: "Sin datos de idiomas.", + fr: "Aucune donnée sur les langues.", + hu: "Nincsenek nyelvi adatok.", + it: "Nessun dato sulle lingue.", + ja: "言語データがありません。", + kr: "언어 데이터가 없습니다.", + nl: "Ingen sprogdata.", + "pt-pt": "Sem dados de idiomas.", + "pt-br": "Sem dados de idiomas.", + np: "कुनै भाषा डाटा छैन।", + el: "Δεν υπάρχουν δεδομένα γλωσσών.", + ru: "Нет данных о языках.", + "uk-ua": "Немає даних про мови.", + id: "Tidak ada data bahasa.", + ml: "ഭാഷാ ഡാറ്റയില്ല.", + my: "Tiada data bahasa.", + sk: "Žiadne údaje o jazykoch.", + tr: "Dil verisi yok.", + pl: "Brak danych dotyczących języków.", + vi: "Không có dữ liệu ngôn ngữ.", + se: "Inga språkdata.", + }, }; const wakatimeCardLocales = { @@ -326,6 +356,22 @@ const wakatimeCardLocales = { vi: "Thống Kê Wakatime", se: "Wakatime statistik", }, + "wakatimecard.lastyear": { + en: "last year", + cn: "去年", + }, + "wakatimecard.last7days": { + en: "last 7 days", + cn: "最近 7 天", + }, + "wakatimecard.notpublic": { + en: "Wakatime user profile not public", + cn: "Wakatime 用户个人资料未公开", + }, + "wakatimecard.nocodedetails": { + en: "User doesn't publicly share detailed code statistics", + cn: "用户不公开分享详细的代码统计信息", + }, "wakatimecard.nocodingactivity": { ar: "لا يوجد نشاط برمجي لهذا الأسبوع", cn: "本周没有编程活动", diff --git a/tests/__snapshots__/renderWakatimeCard.test.js.snap b/tests/__snapshots__/renderWakatimeCard.test.js.snap index 1c0bd701fbbfe0..055c0f6642a7a4 100644 --- a/tests/__snapshots__/renderWakatimeCard.test.js.snap +++ b/tests/__snapshots__/renderWakatimeCard.test.js.snap @@ -69,7 +69,29 @@ exports[`Test Render Wakatime Card should render correctly with compact layout 1 } + @keyframes slideInAnimation { + from { + width: 0; + } + to { + width: calc(100%-100px); + } + } + @keyframes growWidthAnimation { + from { + width: 0; + } + to { + width: 100%; + } + } .lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 } + #rect-mask rect{ + animation: slideInAnimation 1s ease-in-out forwards; + } + .lang-progress{ + animation: growWidthAnimation 0.6s ease-in-out forwards; + } @@ -101,7 +123,7 @@ exports[`Test Render Wakatime Card should render correctly with compact layout 1 y="0" class="header" data-testid="header" - >Wakatime Stats + >Wakatime Stats (last 7 days) @@ -227,7 +249,29 @@ exports[`Test Render Wakatime Card should render correctly with compact layout w } + @keyframes slideInAnimation { + from { + width: 0; + } + to { + width: calc(100%-100px); + } + } + @keyframes growWidthAnimation { + from { + width: 0; + } + to { + width: 100%; + } + } .lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 } + #rect-mask rect{ + animation: slideInAnimation 1s ease-in-out forwards; + } + .lang-progress{ + animation: growWidthAnimation 0.6s ease-in-out forwards; + } @@ -259,7 +303,7 @@ exports[`Test Render Wakatime Card should render correctly with compact layout w y="0" class="header" data-testid="header" - >Wakatime Stats + >Wakatime Stats (last 7 days) diff --git a/tests/api.test.js b/tests/api.test.js index 461f3e18abb6da..0f14378312435a 100644 --- a/tests/api.test.js +++ b/tests/api.test.js @@ -12,17 +12,18 @@ const stats = { totalCommits: 200, totalIssues: 300, totalPRs: 400, - contributedTo: 500, + contributedTo: 50, rank: null, }; + stats.rank = calculateRank({ - totalCommits: stats.totalCommits, - totalRepos: 1, - followers: 0, - contributions: stats.contributedTo, - stargazers: stats.totalStars, + all_commits: false, + commits: stats.totalCommits, prs: stats.totalPRs, issues: stats.totalIssues, + repos: 1, + stars: stats.totalStars, + followers: 0, }); const data_stats = { @@ -43,7 +44,7 @@ const data_stats = { nodes: [{ stargazers: { totalCount: 100 } }], pageInfo: { hasNextPage: false, - cursor: "cursor", + endCursor: "cursor", }, }, }, @@ -229,38 +230,6 @@ describe("Test /api/", () => { } }); - it("should add private contributions", async () => { - const { req, res } = faker( - { - username: "anuraghazra", - count_private: true, - }, - data_stats, - ); - - await api(req, res); - - expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toBeCalledWith( - renderStatsCard( - { - ...stats, - totalCommits: stats.totalCommits + 100, - rank: calculateRank({ - totalCommits: stats.totalCommits + 100, - totalRepos: 1, - followers: 0, - contributions: stats.contributedTo, - stargazers: stats.totalStars, - prs: stats.totalPRs, - issues: stats.totalIssues, - }), - }, - {}, - ), - ); - }); - it("should allow changing ring_color", async () => { const { req, res } = faker( { diff --git a/tests/calculateRank.test.js b/tests/calculateRank.test.js index 235b1b5f20b045..3bfd7f43762481 100644 --- a/tests/calculateRank.test.js +++ b/tests/calculateRank.test.js @@ -2,17 +2,87 @@ import "@testing-library/jest-dom"; import { calculateRank } from "../src/calculateRank.js"; describe("Test calculateRank", () => { - it("should calculate rank correctly", () => { + it("new user gets B rank", () => { expect( calculateRank({ - totalCommits: 100, - totalRepos: 5, + all_commits: false, + commits: 0, + prs: 0, + issues: 0, + repos: 0, + stars: 0, + followers: 0, + }), + ).toStrictEqual({ level: "B", score: 100 }); + }); + + it("average user gets A rank", () => { + expect( + calculateRank({ + all_commits: false, + commits: 250, + prs: 50, + issues: 25, + repos: 0, + stars: 250, + followers: 25, + }), + ).toStrictEqual({ level: "A", score: 50 }); + }); + + it("average user gets A rank (include_all_commits)", () => { + expect( + calculateRank({ + all_commits: true, + commits: 1000, + prs: 50, + issues: 25, + repos: 0, + stars: 250, + followers: 25, + }), + ).toStrictEqual({ level: "A", score: 50 }); + }); + + it("more than average user gets A+ rank", () => { + expect( + calculateRank({ + all_commits: false, + commits: 500, + prs: 100, + issues: 50, + repos: 0, + stars: 500, + followers: 50, + }), + ).toStrictEqual({ level: "A+", score: 25 }); + }); + + it("expert user gets S rank", () => { + expect( + calculateRank({ + all_commits: false, + commits: 1000, + prs: 200, + issues: 100, + repos: 0, + stars: 1000, followers: 100, - contributions: 61, - stargazers: 400, - prs: 300, - issues: 200, }), - ).toStrictEqual({ level: "A+", score: 49.25629684876535 }); + ).toStrictEqual({ level: "S", score: 6.25 }); + }); + + it("ezyang gets S+ rank", () => { + expect( + calculateRank({ + all_commits: false, + commits: 1000, + prs: 4000, + issues: 2000, + repos: 0, + stars: 5000, + followers: 2000, + }), + ).toStrictEqual({ level: "S+", score: 1.1363983154296875 }); }); }); diff --git a/tests/e2e/e2e.test.js b/tests/e2e/e2e.test.js index 402e210fcee17d..e7785975420771 100644 --- a/tests/e2e/e2e.test.js +++ b/tests/e2e/e2e.test.js @@ -15,15 +15,12 @@ const REPO = "curly-fiesta"; const USER = "catelinemnemosyne"; const STATS_DATA = { name: "Cateline Mnemosyne", - totalPRs: 1, - totalCommits: 7, + totalPRs: 2, + totalCommits: 8, totalIssues: 1, totalStars: 1, contributedTo: 1, - rank: { - level: "A+", - score: 50.893750297869225, - }, + rank: { level: "B", score: 98.50610674501908 }, }; const LANGS_DATA = { @@ -54,7 +51,7 @@ const WAKATIME_DATA = { is_up_to_date: false, is_up_to_date_pending_future: false, percent_calculated: 0, - range: "last_7_days", + range: "all_time", status: "pending_update", timeout: 15, username: USER, diff --git a/tests/fetchStats.test.js b/tests/fetchStats.test.js index 04e943a75b50ad..3b27e8a6bc3569 100644 --- a/tests/fetchStats.test.js +++ b/tests/fetchStats.test.js @@ -27,7 +27,7 @@ const data_stats = { ], pageInfo: { hasNextPage: true, - cursor: "cursor", + endCursor: "cursor", }, }, }, @@ -44,7 +44,7 @@ const data_repo = { ], pageInfo: { hasNextPage: false, - cursor: "cursor", + endCursor: "cursor", }, }, }, @@ -64,7 +64,7 @@ const data_repo_zero_stars = { ], pageInfo: { hasNextPage: true, - cursor: "cursor", + endCursor: "cursor", }, }, }, @@ -86,11 +86,12 @@ const mock = new MockAdapter(axios); beforeEach(() => { process.env.FETCH_MULTI_PAGE_STARS = "false"; // Set to `false` to fetch only one page of stars. - mock - .onPost("https://api.github.com/graphql") - .replyOnce(200, data_stats) - .onPost("https://api.github.com/graphql") - .replyOnce(200, data_repo); + mock.onPost("https://api.github.com/graphql").reply((cfg) => { + return [ + 200, + cfg.data.includes("contributionsCollection") ? data_stats : data_repo, + ]; + }); }); afterEach(() => { @@ -101,13 +102,13 @@ describe("Test fetchStats", () => { it("should fetch correct stats", async () => { let stats = await fetchStats("anuraghazra"); const rank = calculateRank({ - totalCommits: 100, - totalRepos: 5, - followers: 100, - contributions: 61, - stargazers: 300, + all_commits: false, + commits: 100, prs: 300, issues: 200, + repos: 5, + stars: 300, + followers: 100, }); expect(stats).toStrictEqual({ @@ -131,13 +132,13 @@ describe("Test fetchStats", () => { let stats = await fetchStats("anuraghazra"); const rank = calculateRank({ - totalCommits: 100, - totalRepos: 5, - followers: 100, - contributions: 61, - stargazers: 300, + all_commits: false, + commits: 100, prs: 300, issues: 200, + repos: 5, + stars: 300, + followers: 100, }); expect(stats).toStrictEqual({ @@ -160,49 +161,26 @@ describe("Test fetchStats", () => { ); }); - it("should fetch and add private contributions", async () => { - let stats = await fetchStats("anuraghazra", true); - const rank = calculateRank({ - totalCommits: 150, - totalRepos: 5, - followers: 100, - contributions: 61, - stargazers: 300, - prs: 300, - issues: 200, - }); - - expect(stats).toStrictEqual({ - contributedTo: 61, - name: "Anurag Hazra", - totalCommits: 150, - totalIssues: 200, - totalPRs: 300, - totalStars: 300, - rank, - }); - }); - it("should fetch total commits", async () => { mock .onGet("https://api.github.com/search/commits?q=author:anuraghazra") .reply(200, { total_count: 1000 }); - let stats = await fetchStats("anuraghazra", true, true); + let stats = await fetchStats("anuraghazra", false, true); const rank = calculateRank({ - totalCommits: 1050, - totalRepos: 5, - followers: 100, - contributions: 61, - stargazers: 300, + all_commits: true, + commits: 1000, prs: 300, issues: 200, + repos: 5, + stars: 300, + followers: 100, }); expect(stats).toStrictEqual({ contributedTo: 61, name: "Anurag Hazra", - totalCommits: 1050, + totalCommits: 1000, totalIssues: 200, totalPRs: 300, totalStars: 300, @@ -215,21 +193,21 @@ describe("Test fetchStats", () => { .onGet("https://api.github.com/search/commits?q=author:anuraghazra") .reply(200, { total_count: 1000 }); - let stats = await fetchStats("anuraghazra", true, true, ["test-repo-1"]); + let stats = await fetchStats("anuraghazra", false, true, ["test-repo-1"]); const rank = calculateRank({ - totalCommits: 1050, - totalRepos: 5, - followers: 100, - contributions: 61, - stargazers: 200, + all_commits: true, + commits: 1000, prs: 300, issues: 200, + repos: 5, + stars: 200, + followers: 100, }); expect(stats).toStrictEqual({ contributedTo: 61, name: "Anurag Hazra", - totalCommits: 1050, + totalCommits: 1000, totalIssues: 200, totalPRs: 300, totalStars: 200, @@ -242,13 +220,13 @@ describe("Test fetchStats", () => { let stats = await fetchStats("anuraghazra"); const rank = calculateRank({ - totalCommits: 100, - totalRepos: 5, - followers: 100, - contributions: 61, - stargazers: 400, + all_commits: false, + commits: 100, prs: 300, issues: 200, + repos: 5, + stars: 400, + followers: 100, }); expect(stats).toStrictEqual({ @@ -267,13 +245,13 @@ describe("Test fetchStats", () => { let stats = await fetchStats("anuraghazra"); const rank = calculateRank({ - totalCommits: 100, - totalRepos: 5, - followers: 100, - contributions: 61, - stargazers: 300, + all_commits: false, + commits: 100, prs: 300, issues: 200, + repos: 5, + stars: 300, + followers: 100, }); expect(stats).toStrictEqual({ @@ -292,13 +270,13 @@ describe("Test fetchStats", () => { let stats = await fetchStats("anuraghazra"); const rank = calculateRank({ - totalCommits: 100, - totalRepos: 5, - followers: 100, - contributions: 61, - stargazers: 300, + all_commits: false, + commits: 100, prs: 300, issues: 200, + repos: 5, + stars: 300, + followers: 100, }); expect(stats).toStrictEqual({ diff --git a/tests/fetchTopLanguages.test.js b/tests/fetchTopLanguages.test.js index 24416cd294525e..c3f558bf4236ff 100644 --- a/tests/fetchTopLanguages.test.js +++ b/tests/fetchTopLanguages.test.js @@ -60,20 +60,22 @@ const error = { }; describe("FetchTopLanguages", () => { - it("should fetch correct language data", async () => { + it("should fetch correct language data while using the new calculation", async () => { mock.onPost("https://api.github.com/graphql").reply(200, data_langs); - let repo = await fetchTopLanguages("anuraghazra"); + let repo = await fetchTopLanguages("anuraghazra", [], 0.5, 0.5); expect(repo).toStrictEqual({ HTML: { color: "#0f0", + count: 2, name: "HTML", - size: 200, + size: 20.000000000000004, }, javascript: { color: "#0ff", + count: 2, name: "javascript", - size: 200, + size: 20.000000000000004, }, }); }); @@ -85,17 +87,59 @@ describe("FetchTopLanguages", () => { expect(repo).toStrictEqual({ HTML: { color: "#0f0", + count: 1, name: "HTML", size: 100, }, javascript: { color: "#0ff", + count: 2, + name: "javascript", + size: 200, + }, + }); + }); + + it("should fetch correct language data while using the old calculation", async () => { + mock.onPost("https://api.github.com/graphql").reply(200, data_langs); + + let repo = await fetchTopLanguages("anuraghazra", [], 1, 0); + expect(repo).toStrictEqual({ + HTML: { + color: "#0f0", + count: 2, + name: "HTML", + size: 200, + }, + javascript: { + color: "#0ff", + count: 2, name: "javascript", size: 200, }, }); }); + it("should rank languages by the number of repositories they appear in", async () => { + mock.onPost("https://api.github.com/graphql").reply(200, data_langs); + + let repo = await fetchTopLanguages("anuraghazra", [], 0, 1); + expect(repo).toStrictEqual({ + HTML: { + color: "#0f0", + count: 2, + name: "HTML", + size: 2, + }, + javascript: { + color: "#0ff", + count: 2, + name: "javascript", + size: 2, + }, + }); + }); + it("should throw error", async () => { mock.onPost("https://api.github.com/graphql").reply(200, error); diff --git a/tests/fetchWakatime.test.js b/tests/fetchWakatime.test.js index 47ca25b7254a6c..964b37dcd10922 100644 --- a/tests/fetchWakatime.test.js +++ b/tests/fetchWakatime.test.js @@ -105,7 +105,7 @@ describe("Wakatime fetcher", () => { const username = "anuraghazra"; mock .onGet( - `https://wakatime.com/api/v1/users/${username}/stats/?is_including_today=true`, + `https://wakatime.com/api/v1/users/${username}/stats?is_including_today=true`, ) .reply(200, wakaTimeData); diff --git a/tests/pat-info.test.js b/tests/pat-info.test.js new file mode 100644 index 00000000000000..23aca8c40e5ca3 --- /dev/null +++ b/tests/pat-info.test.js @@ -0,0 +1,244 @@ +/** + * @file Tests for the status/pat-info cloud function. + */ +import dotenv from "dotenv"; +dotenv.config(); + +import { jest } from "@jest/globals"; +import axios from "axios"; +import MockAdapter from "axios-mock-adapter"; +import patInfo, { RATE_LIMIT_SECONDS } from "../api/status/pat-info.js"; + +const mock = new MockAdapter(axios); + +const successData = { + data: { + rateLimit: { + remaining: 4986, + }, + }, +}; + +const faker = (query) => { + const req = { + query: { ...query }, + }; + const res = { + setHeader: jest.fn(), + send: jest.fn(), + }; + + return { req, res }; +}; + +const rate_limit_error = { + errors: [ + { + type: "RATE_LIMITED", + message: "API rate limit exceeded for user ID.", + }, + ], + data: { + rateLimit: { + resetAt: Date.now(), + }, + }, +}; + +const other_error = { + errors: [ + { + type: "SOME_ERROR", + message: "This is a error", + }, + ], +}; + +const bad_credentials_error = { + message: "Bad credentials", +}; + +afterEach(() => { + mock.reset(); +}); + +describe("Test /api/status/pat-info", () => { + beforeAll(() => { + // reset patenv first so that dotenv doesn't populate them with local envs + process.env = {}; + process.env.PAT_1 = "testPAT1"; + process.env.PAT_2 = "testPAT2"; + process.env.PAT_3 = "testPAT3"; + process.env.PAT_4 = "testPAT4"; + }); + + it("should return only 'validPATs' if all PATs are valid", async () => { + mock + .onPost("https://api.github.com/graphql") + .replyOnce(200, rate_limit_error) + .onPost("https://api.github.com/graphql") + .reply(200, successData); + + const { req, res } = faker({}, {}); + await patInfo(req, res); + + expect(res.setHeader).toBeCalledWith("Content-Type", "application/json"); + expect(res.send).toBeCalledWith( + JSON.stringify( + { + validPATs: ["PAT_2", "PAT_3", "PAT_4"], + expiredPATs: [], + exhaustedPATs: ["PAT_1"], + suspendedPATs: [], + errorPATs: [], + details: { + PAT_1: { + status: "exhausted", + remaining: 0, + resetIn: "0 minutes", + }, + PAT_2: { + status: "valid", + remaining: 4986, + }, + PAT_3: { + status: "valid", + remaining: 4986, + }, + PAT_4: { + status: "valid", + remaining: 4986, + }, + }, + }, + null, + 2, + ), + ); + }); + + it("should return `errorPATs` if a PAT causes an error to be thrown", async () => { + mock + .onPost("https://api.github.com/graphql") + .replyOnce(200, other_error) + .onPost("https://api.github.com/graphql") + .reply(200, successData); + + const { req, res } = faker({}, {}); + await patInfo(req, res); + + expect(res.setHeader).toBeCalledWith("Content-Type", "application/json"); + expect(res.send).toBeCalledWith( + JSON.stringify( + { + validPATs: ["PAT_2", "PAT_3", "PAT_4"], + expiredPATs: [], + exhaustedPATs: [], + suspendedPATs: [], + errorPATs: ["PAT_1"], + details: { + PAT_1: { + status: "error", + error: { + type: "SOME_ERROR", + message: "This is a error", + }, + }, + PAT_2: { + status: "valid", + remaining: 4986, + }, + PAT_3: { + status: "valid", + remaining: 4986, + }, + PAT_4: { + status: "valid", + remaining: 4986, + }, + }, + }, + null, + 2, + ), + ); + }); + + it("should return `expiredPaths` if a PAT returns a 'Bad credentials' error", async () => { + mock + .onPost("https://api.github.com/graphql") + .replyOnce(404, bad_credentials_error) + .onPost("https://api.github.com/graphql") + .reply(200, successData); + + const { req, res } = faker({}, {}); + await patInfo(req, res); + + expect(res.setHeader).toBeCalledWith("Content-Type", "application/json"); + expect(res.send).toBeCalledWith( + JSON.stringify( + { + validPATs: ["PAT_2", "PAT_3", "PAT_4"], + expiredPATs: ["PAT_1"], + exhaustedPATs: [], + suspendedPATs: [], + errorPATs: [], + details: { + PAT_1: { + status: "expired", + }, + PAT_2: { + status: "valid", + remaining: 4986, + }, + PAT_3: { + status: "valid", + remaining: 4986, + }, + PAT_4: { + status: "valid", + remaining: 4986, + }, + }, + }, + null, + 2, + ), + ); + }); + + it("should throw an error if something goes wrong", async () => { + mock.onPost("https://api.github.com/graphql").networkError(); + + const { req, res } = faker({}, {}); + await patInfo(req, res); + + expect(res.setHeader).toBeCalledWith("Content-Type", "application/json"); + expect(res.send).toBeCalledWith("Something went wrong: Network Error"); + }); + + it("should have proper cache when no error is thrown", async () => { + mock.onPost("https://api.github.com/graphql").reply(200, successData); + + const { req, res } = faker({}, {}); + await patInfo(req, res); + + expect(res.setHeader.mock.calls).toEqual([ + ["Content-Type", "application/json"], + ["Cache-Control", `max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`], + ]); + }); + + it("should have proper cache when error is thrown", async () => { + mock.reset(); + mock.onPost("https://api.github.com/graphql").networkError(); + + const { req, res } = faker({}, {}); + await patInfo(req, res); + + expect(res.setHeader.mock.calls).toEqual([ + ["Content-Type", "application/json"], + ["Cache-Control", "no-store"], + ]); + }); +}); diff --git a/tests/renderStatsCard.test.js b/tests/renderStatsCard.test.js index 748b7a32cd32b6..25c5feb6048645 100644 --- a/tests/renderStatsCard.test.js +++ b/tests/renderStatsCard.test.js @@ -357,4 +357,30 @@ describe("Test renderStatsCard", () => { document.body.innerHTML = renderStatsCard(stats, {}); expect(document.querySelector("rect")).toHaveAttribute("rx", "4.5"); }); + + it("should shorten values", () => { + stats["totalCommits"] = 1999; + + document.body.innerHTML = renderStatsCard(stats); + expect(getByTestId(document.body, "commits").textContent).toBe("2k"); + document.body.innerHTML = renderStatsCard(stats, { number_format: "long" }); + expect(getByTestId(document.body, "commits").textContent).toBe("1999"); + }); + + it("should render default rank icon with level A+", () => { + document.body.innerHTML = renderStatsCard(stats, { + rank_icon: "default", + }); + expect(queryByTestId(document.body, "level-rank-icon")).toBeDefined(); + expect( + queryByTestId(document.body, "level-rank-icon").textContent.trim(), + ).toBe("A+"); + }); + + it("should render github rank icon", () => { + document.body.innerHTML = renderStatsCard(stats, { + rank_icon: "github", + }); + expect(queryByTestId(document.body, "github-rank-icon")).toBeDefined(); + }); }); diff --git a/tests/renderTopLanguages.test.js b/tests/renderTopLanguages.test.js index 8ae4bbd0c16e6c..ed3bd3d76973ca 100644 --- a/tests/renderTopLanguages.test.js +++ b/tests/renderTopLanguages.test.js @@ -1,9 +1,23 @@ import { queryAllByTestId, queryByTestId } from "@testing-library/dom"; import { cssToObject } from "@uppercod/css-to-object"; import { - MIN_CARD_WIDTH, + getLongestLang, + degreesToRadians, + radiansToDegrees, + polarToCartesian, + cartesianToPolar, + getCircleLength, + calculateCompactLayoutHeight, + calculateNormalLayoutHeight, + calculateDonutLayoutHeight, + calculateDonutVerticalLayoutHeight, + calculatePieLayoutHeight, + donutCenterTranslation, + trimTopLanguages, renderTopLanguages, + MIN_CARD_WIDTH, } from "../src/cards/top-languages-card.js"; + // adds special assertions like toHaveTextContent import "@testing-library/jest-dom"; @@ -27,6 +41,295 @@ const langs = { }, }; +/** + * Retrieve number array from SVG path definition string. + * + * @param {string} d SVG path definition string. + * @return {number[]} Resulting numbers array. + */ +const getNumbersFromSvgPathDefinitionAttribute = (d) => { + return d + .split(" ") + .filter((x) => !isNaN(x)) + .map((x) => parseFloat(x)); +}; + +/** + * Retrieve the language percentage from the donut chart SVG. + * + * @param {string} d The SVG path element. + * @param {number} centerX The center X coordinate of the donut chart. + * @param {number} centerY The center Y coordinate of the donut chart. + * @returns {number} The percentage of the language. + */ +const langPercentFromDonutLayoutSvg = (d, centerX, centerY) => { + const dTmp = getNumbersFromSvgPathDefinitionAttribute(d); + const endAngle = + cartesianToPolar(centerX, centerY, dTmp[0], dTmp[1]).angleInDegrees + 90; + let startAngle = + cartesianToPolar(centerX, centerY, dTmp[7], dTmp[8]).angleInDegrees + 90; + if (startAngle > endAngle) startAngle -= 360; + return (endAngle - startAngle) / 3.6; +}; + +/** + * Calculate language percentage for donut vertical chart SVG. + * + * @param {number} partLength Length of current chart part.. + * @param {number} totalCircleLength Total length of circle. + * @return {number} Chart part percentage. + */ +const langPercentFromDonutVerticalLayoutSvg = ( + partLength, + totalCircleLength, +) => { + return (partLength / totalCircleLength) * 100; +}; + +/** + * Retrieve the language percentage from the pie chart SVG. + * + * @param {string} d The SVG path element. + * @param {number} centerX The center X coordinate of the pie chart. + * @param {number} centerY The center Y coordinate of the pie chart. + * @returns {number} The percentage of the language. + */ +const langPercentFromPieLayoutSvg = (d, centerX, centerY) => { + const dTmp = getNumbersFromSvgPathDefinitionAttribute(d); + const startAngle = cartesianToPolar( + centerX, + centerY, + dTmp[2], + dTmp[3], + ).angleInDegrees; + let endAngle = cartesianToPolar( + centerX, + centerY, + dTmp[9], + dTmp[10], + ).angleInDegrees; + return ((endAngle - startAngle) / 360) * 100; +}; + +describe("Test renderTopLanguages helper functions", () => { + it("getLongestLang", () => { + const langArray = Object.values(langs); + expect(getLongestLang(langArray)).toBe(langs.javascript); + }); + + it("degreesToRadians", () => { + expect(degreesToRadians(0)).toBe(0); + expect(degreesToRadians(90)).toBe(Math.PI / 2); + expect(degreesToRadians(180)).toBe(Math.PI); + expect(degreesToRadians(270)).toBe((3 * Math.PI) / 2); + expect(degreesToRadians(360)).toBe(2 * Math.PI); + }); + + it("radiansToDegrees", () => { + expect(radiansToDegrees(0)).toBe(0); + expect(radiansToDegrees(Math.PI / 2)).toBe(90); + expect(radiansToDegrees(Math.PI)).toBe(180); + expect(radiansToDegrees((3 * Math.PI) / 2)).toBe(270); + expect(radiansToDegrees(2 * Math.PI)).toBe(360); + }); + + it("polarToCartesian", () => { + expect(polarToCartesian(100, 100, 60, 0)).toStrictEqual({ x: 160, y: 100 }); + expect(polarToCartesian(100, 100, 60, 45)).toStrictEqual({ + x: 142.42640687119285, + y: 142.42640687119285, + }); + expect(polarToCartesian(100, 100, 60, 90)).toStrictEqual({ + x: 100, + y: 160, + }); + expect(polarToCartesian(100, 100, 60, 135)).toStrictEqual({ + x: 57.573593128807154, + y: 142.42640687119285, + }); + expect(polarToCartesian(100, 100, 60, 180)).toStrictEqual({ + x: 40, + y: 100.00000000000001, + }); + expect(polarToCartesian(100, 100, 60, 225)).toStrictEqual({ + x: 57.57359312880714, + y: 57.573593128807154, + }); + expect(polarToCartesian(100, 100, 60, 270)).toStrictEqual({ + x: 99.99999999999999, + y: 40, + }); + expect(polarToCartesian(100, 100, 60, 315)).toStrictEqual({ + x: 142.42640687119285, + y: 57.57359312880714, + }); + expect(polarToCartesian(100, 100, 60, 360)).toStrictEqual({ + x: 160, + y: 99.99999999999999, + }); + }); + + it("cartesianToPolar", () => { + expect(cartesianToPolar(100, 100, 160, 100)).toStrictEqual({ + radius: 60, + angleInDegrees: 0, + }); + expect( + cartesianToPolar(100, 100, 142.42640687119285, 142.42640687119285), + ).toStrictEqual({ radius: 60.00000000000001, angleInDegrees: 45 }); + expect(cartesianToPolar(100, 100, 100, 160)).toStrictEqual({ + radius: 60, + angleInDegrees: 90, + }); + expect( + cartesianToPolar(100, 100, 57.573593128807154, 142.42640687119285), + ).toStrictEqual({ radius: 60, angleInDegrees: 135 }); + expect(cartesianToPolar(100, 100, 40, 100.00000000000001)).toStrictEqual({ + radius: 60, + angleInDegrees: 180, + }); + expect( + cartesianToPolar(100, 100, 57.57359312880714, 57.573593128807154), + ).toStrictEqual({ radius: 60, angleInDegrees: 225 }); + expect(cartesianToPolar(100, 100, 99.99999999999999, 40)).toStrictEqual({ + radius: 60, + angleInDegrees: 270, + }); + expect( + cartesianToPolar(100, 100, 142.42640687119285, 57.57359312880714), + ).toStrictEqual({ radius: 60.00000000000001, angleInDegrees: 315 }); + expect(cartesianToPolar(100, 100, 160, 99.99999999999999)).toStrictEqual({ + radius: 60, + angleInDegrees: 360, + }); + }); + + it("calculateCompactLayoutHeight", () => { + expect(calculateCompactLayoutHeight(0)).toBe(90); + expect(calculateCompactLayoutHeight(1)).toBe(115); + expect(calculateCompactLayoutHeight(2)).toBe(115); + expect(calculateCompactLayoutHeight(3)).toBe(140); + expect(calculateCompactLayoutHeight(4)).toBe(140); + expect(calculateCompactLayoutHeight(5)).toBe(165); + expect(calculateCompactLayoutHeight(6)).toBe(165); + expect(calculateCompactLayoutHeight(7)).toBe(190); + expect(calculateCompactLayoutHeight(8)).toBe(190); + expect(calculateCompactLayoutHeight(9)).toBe(215); + expect(calculateCompactLayoutHeight(10)).toBe(215); + }); + + it("calculateNormalLayoutHeight", () => { + expect(calculateNormalLayoutHeight(0)).toBe(85); + expect(calculateNormalLayoutHeight(1)).toBe(125); + expect(calculateNormalLayoutHeight(2)).toBe(165); + expect(calculateNormalLayoutHeight(3)).toBe(205); + expect(calculateNormalLayoutHeight(4)).toBe(245); + expect(calculateNormalLayoutHeight(5)).toBe(285); + expect(calculateNormalLayoutHeight(6)).toBe(325); + expect(calculateNormalLayoutHeight(7)).toBe(365); + expect(calculateNormalLayoutHeight(8)).toBe(405); + expect(calculateNormalLayoutHeight(9)).toBe(445); + expect(calculateNormalLayoutHeight(10)).toBe(485); + }); + + it("calculateDonutLayoutHeight", () => { + expect(calculateDonutLayoutHeight(0)).toBe(215); + expect(calculateDonutLayoutHeight(1)).toBe(215); + expect(calculateDonutLayoutHeight(2)).toBe(215); + expect(calculateDonutLayoutHeight(3)).toBe(215); + expect(calculateDonutLayoutHeight(4)).toBe(215); + expect(calculateDonutLayoutHeight(5)).toBe(215); + expect(calculateDonutLayoutHeight(6)).toBe(247); + expect(calculateDonutLayoutHeight(7)).toBe(279); + expect(calculateDonutLayoutHeight(8)).toBe(311); + expect(calculateDonutLayoutHeight(9)).toBe(343); + expect(calculateDonutLayoutHeight(10)).toBe(375); + }); + + it("calculateDonutVerticalLayoutHeight", () => { + expect(calculateDonutVerticalLayoutHeight(0)).toBe(300); + expect(calculateDonutVerticalLayoutHeight(1)).toBe(325); + expect(calculateDonutVerticalLayoutHeight(2)).toBe(325); + expect(calculateDonutVerticalLayoutHeight(3)).toBe(350); + expect(calculateDonutVerticalLayoutHeight(4)).toBe(350); + expect(calculateDonutVerticalLayoutHeight(5)).toBe(375); + expect(calculateDonutVerticalLayoutHeight(6)).toBe(375); + expect(calculateDonutVerticalLayoutHeight(7)).toBe(400); + expect(calculateDonutVerticalLayoutHeight(8)).toBe(400); + expect(calculateDonutVerticalLayoutHeight(9)).toBe(425); + expect(calculateDonutVerticalLayoutHeight(10)).toBe(425); + }); + + it("calculatePieLayoutHeight", () => { + expect(calculatePieLayoutHeight(0)).toBe(300); + expect(calculatePieLayoutHeight(1)).toBe(325); + expect(calculatePieLayoutHeight(2)).toBe(325); + expect(calculatePieLayoutHeight(3)).toBe(350); + expect(calculatePieLayoutHeight(4)).toBe(350); + expect(calculatePieLayoutHeight(5)).toBe(375); + expect(calculatePieLayoutHeight(6)).toBe(375); + expect(calculatePieLayoutHeight(7)).toBe(400); + expect(calculatePieLayoutHeight(8)).toBe(400); + expect(calculatePieLayoutHeight(9)).toBe(425); + expect(calculatePieLayoutHeight(10)).toBe(425); + }); + + it("donutCenterTranslation", () => { + expect(donutCenterTranslation(0)).toBe(-45); + expect(donutCenterTranslation(1)).toBe(-45); + expect(donutCenterTranslation(2)).toBe(-45); + expect(donutCenterTranslation(3)).toBe(-45); + expect(donutCenterTranslation(4)).toBe(-45); + expect(donutCenterTranslation(5)).toBe(-45); + expect(donutCenterTranslation(6)).toBe(-29); + expect(donutCenterTranslation(7)).toBe(-13); + expect(donutCenterTranslation(8)).toBe(3); + expect(donutCenterTranslation(9)).toBe(19); + expect(donutCenterTranslation(10)).toBe(35); + }); + + it("getCircleLength", () => { + expect(getCircleLength(20)).toBeCloseTo(125.663); + expect(getCircleLength(30)).toBeCloseTo(188.495); + expect(getCircleLength(40)).toBeCloseTo(251.327); + expect(getCircleLength(50)).toBeCloseTo(314.159); + expect(getCircleLength(60)).toBeCloseTo(376.991); + expect(getCircleLength(70)).toBeCloseTo(439.822); + expect(getCircleLength(80)).toBeCloseTo(502.654); + expect(getCircleLength(90)).toBeCloseTo(565.486); + expect(getCircleLength(100)).toBeCloseTo(628.318); + }); + + it("trimTopLanguages", () => { + expect(trimTopLanguages([])).toStrictEqual({ + langs: [], + totalLanguageSize: 0, + }); + expect(trimTopLanguages([langs.javascript])).toStrictEqual({ + langs: [langs.javascript], + totalLanguageSize: 200, + }); + expect( + trimTopLanguages([langs.javascript, langs.HTML], [], 5), + ).toStrictEqual({ + langs: [langs.javascript, langs.HTML], + totalLanguageSize: 400, + }); + expect(trimTopLanguages(langs, [], 5)).toStrictEqual({ + langs: Object.values(langs), + totalLanguageSize: 500, + }); + expect(trimTopLanguages(langs, [], 2)).toStrictEqual({ + langs: Object.values(langs).slice(0, 2), + totalLanguageSize: 400, + }); + expect(trimTopLanguages(langs, ["javascript"], 5)).toStrictEqual({ + langs: [langs.HTML, langs.css], + totalLanguageSize: 300, + }); + }); +}); + describe("Test renderTopLanguages", () => { it("should render correctly", () => { document.body.innerHTML = renderTopLanguages(langs); @@ -216,7 +519,7 @@ describe("Test renderTopLanguages", () => { ); expect(queryAllByTestId(document.body, "lang-progress")[0]).toHaveAttribute( "width", - "120", + "100", ); expect(queryAllByTestId(document.body, "lang-name")[1]).toHaveTextContent( @@ -224,7 +527,7 @@ describe("Test renderTopLanguages", () => { ); expect(queryAllByTestId(document.body, "lang-progress")[1]).toHaveAttribute( "width", - "120", + "100", ); expect(queryAllByTestId(document.body, "lang-name")[2]).toHaveTextContent( @@ -232,7 +535,251 @@ describe("Test renderTopLanguages", () => { ); expect(queryAllByTestId(document.body, "lang-progress")[2]).toHaveAttribute( "width", - "60", + "50", + ); + }); + + it("should render with layout donut", () => { + document.body.innerHTML = renderTopLanguages(langs, { layout: "donut" }); + + expect(queryByTestId(document.body, "header")).toHaveTextContent( + "Most Used Languages", + ); + + expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent( + "HTML 40.00%", + ); + expect(queryAllByTestId(document.body, "lang-donut")[0]).toHaveAttribute( + "size", + "40", + ); + const d = getNumbersFromSvgPathDefinitionAttribute( + queryAllByTestId(document.body, "lang-donut")[0].getAttribute("d"), + ); + const center = { x: d[7], y: d[7] }; + const HTMLLangPercent = langPercentFromDonutLayoutSvg( + queryAllByTestId(document.body, "lang-donut")[0].getAttribute("d"), + center.x, + center.y, + ); + expect(HTMLLangPercent).toBeCloseTo(40); + + expect(queryAllByTestId(document.body, "lang-name")[1]).toHaveTextContent( + "javascript 40.00%", + ); + expect(queryAllByTestId(document.body, "lang-donut")[1]).toHaveAttribute( + "size", + "40", + ); + const javascriptLangPercent = langPercentFromDonutLayoutSvg( + queryAllByTestId(document.body, "lang-donut")[1].getAttribute("d"), + center.x, + center.y, + ); + expect(javascriptLangPercent).toBeCloseTo(40); + + expect(queryAllByTestId(document.body, "lang-name")[2]).toHaveTextContent( + "css 20.00%", + ); + expect(queryAllByTestId(document.body, "lang-donut")[2]).toHaveAttribute( + "size", + "20", + ); + const cssLangPercent = langPercentFromDonutLayoutSvg( + queryAllByTestId(document.body, "lang-donut")[2].getAttribute("d"), + center.x, + center.y, + ); + expect(cssLangPercent).toBeCloseTo(20); + + expect(HTMLLangPercent + javascriptLangPercent + cssLangPercent).toBe(100); + + // Should render full donut (circle) if one language is 100%. + document.body.innerHTML = renderTopLanguages( + { HTML: langs.HTML }, + { layout: "donut" }, + ); + expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent( + "HTML 100.00%", + ); + expect(queryAllByTestId(document.body, "lang-donut")[0]).toHaveAttribute( + "size", + "100", + ); + expect(queryAllByTestId(document.body, "lang-donut")).toHaveLength(1); + expect(queryAllByTestId(document.body, "lang-donut")[0].tagName).toBe( + "circle", + ); + }); + + it("should render with layout donut vertical", () => { + document.body.innerHTML = renderTopLanguages(langs, { + layout: "donut-vertical", + }); + + expect(queryByTestId(document.body, "header")).toHaveTextContent( + "Most Used Languages", + ); + + expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent( + "HTML 40.00%", + ); + expect(queryAllByTestId(document.body, "lang-donut")[0]).toHaveAttribute( + "size", + "40", + ); + + const totalCircleLength = queryAllByTestId( + document.body, + "lang-donut", + )[0].getAttribute("stroke-dasharray"); + + const HTMLLangPercent = langPercentFromDonutVerticalLayoutSvg( + queryAllByTestId(document.body, "lang-donut")[1].getAttribute( + "stroke-dashoffset", + ) - + queryAllByTestId(document.body, "lang-donut")[0].getAttribute( + "stroke-dashoffset", + ), + totalCircleLength, + ); + expect(HTMLLangPercent).toBeCloseTo(40); + + expect(queryAllByTestId(document.body, "lang-name")[1]).toHaveTextContent( + "javascript 40.00%", + ); + expect(queryAllByTestId(document.body, "lang-donut")[1]).toHaveAttribute( + "size", + "40", + ); + const javascriptLangPercent = langPercentFromDonutVerticalLayoutSvg( + queryAllByTestId(document.body, "lang-donut")[2].getAttribute( + "stroke-dashoffset", + ) - + queryAllByTestId(document.body, "lang-donut")[1].getAttribute( + "stroke-dashoffset", + ), + totalCircleLength, + ); + expect(javascriptLangPercent).toBeCloseTo(40); + + expect(queryAllByTestId(document.body, "lang-name")[2]).toHaveTextContent( + "css 20.00%", + ); + expect(queryAllByTestId(document.body, "lang-donut")[2]).toHaveAttribute( + "size", + "20", + ); + const cssLangPercent = langPercentFromDonutVerticalLayoutSvg( + totalCircleLength - + queryAllByTestId(document.body, "lang-donut")[2].getAttribute( + "stroke-dashoffset", + ), + totalCircleLength, + ); + expect(cssLangPercent).toBeCloseTo(20); + + expect(HTMLLangPercent + javascriptLangPercent + cssLangPercent).toBe(100); + }); + + it("should render with layout donut vertical full donut circle of one language is 100%", () => { + document.body.innerHTML = renderTopLanguages( + { HTML: langs.HTML }, + { layout: "donut-vertical" }, + ); + expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent( + "HTML 100.00%", + ); + expect(queryAllByTestId(document.body, "lang-donut")[0]).toHaveAttribute( + "size", + "100", + ); + const totalCircleLength = queryAllByTestId( + document.body, + "lang-donut", + )[0].getAttribute("stroke-dasharray"); + + const HTMLLangPercent = langPercentFromDonutVerticalLayoutSvg( + totalCircleLength - + queryAllByTestId(document.body, "lang-donut")[0].getAttribute( + "stroke-dashoffset", + ), + totalCircleLength, + ); + expect(HTMLLangPercent).toBeCloseTo(100); + }); + + it("should render with layout pie", () => { + document.body.innerHTML = renderTopLanguages(langs, { layout: "pie" }); + + expect(queryByTestId(document.body, "header")).toHaveTextContent( + "Most Used Languages", + ); + + expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent( + "HTML 40.00%", + ); + expect(queryAllByTestId(document.body, "lang-pie")[0]).toHaveAttribute( + "size", + "40", + ); + + const d = getNumbersFromSvgPathDefinitionAttribute( + queryAllByTestId(document.body, "lang-pie")[0].getAttribute("d"), + ); + const center = { x: d[0], y: d[1] }; + const HTMLLangPercent = langPercentFromPieLayoutSvg( + queryAllByTestId(document.body, "lang-pie")[0].getAttribute("d"), + center.x, + center.y, + ); + expect(HTMLLangPercent).toBeCloseTo(40); + + expect(queryAllByTestId(document.body, "lang-name")[1]).toHaveTextContent( + "javascript 40.00%", + ); + expect(queryAllByTestId(document.body, "lang-pie")[1]).toHaveAttribute( + "size", + "40", + ); + const javascriptLangPercent = langPercentFromPieLayoutSvg( + queryAllByTestId(document.body, "lang-pie")[1].getAttribute("d"), + center.x, + center.y, + ); + expect(javascriptLangPercent).toBeCloseTo(40); + + expect(queryAllByTestId(document.body, "lang-name")[2]).toHaveTextContent( + "css 20.00%", + ); + expect(queryAllByTestId(document.body, "lang-pie")[2]).toHaveAttribute( + "size", + "20", + ); + const cssLangPercent = langPercentFromPieLayoutSvg( + queryAllByTestId(document.body, "lang-pie")[2].getAttribute("d"), + center.x, + center.y, + ); + expect(cssLangPercent).toBeCloseTo(20); + + expect(HTMLLangPercent + javascriptLangPercent + cssLangPercent).toBe(100); + + // Should render full pie (circle) if one language is 100%. + document.body.innerHTML = renderTopLanguages( + { HTML: langs.HTML }, + { layout: "pie" }, + ); + expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent( + "HTML 100.00%", + ); + expect(queryAllByTestId(document.body, "lang-pie")[0]).toHaveAttribute( + "size", + "100", + ); + expect(queryAllByTestId(document.body, "lang-pie")).toHaveLength(1); + expect(queryAllByTestId(document.body, "lang-pie")[0].tagName).toBe( + "circle", ); }); diff --git a/tests/renderWakatimeCard.test.js b/tests/renderWakatimeCard.test.js index 67969bef500637..7553277f60bac0 100644 --- a/tests/renderWakatimeCard.test.js +++ b/tests/renderWakatimeCard.test.js @@ -43,7 +43,7 @@ describe("Test Render Wakatime Card", () => { expect( document.querySelector('g[transform="translate(0, 0)"]>text.stat.bold') .textContent, - ).toBe("本周没有编程活动"); + ).toBe("Wakatime 用户个人资料未公开"); }); it("should render without rounding", () => { @@ -55,7 +55,7 @@ describe("Test Render Wakatime Card", () => { expect(document.querySelector("rect")).toHaveAttribute("rx", "4.5"); }); - it('should show "no coding activitiy this week" message when there hasn not been activity', () => { + it('should show "no coding activity this week" message when there has not been activity', () => { document.body.innerHTML = renderWakatimeCard( { ...wakaTimeData.data, @@ -67,4 +67,19 @@ describe("Test Render Wakatime Card", () => { "No coding activity this week", ); }); + + it('should show "no coding activity this week" message when using compact layout and there has not been activity', () => { + document.body.innerHTML = renderWakatimeCard( + { + ...wakaTimeData.data, + languages: undefined, + }, + { + layout: "compact", + }, + ); + expect(document.querySelector(".stat").textContent).toBe( + "No coding activity this week", + ); + }); }); diff --git a/tests/status.up.test.js b/tests/status.up.test.js new file mode 100644 index 00000000000000..7cf0144b7112dc --- /dev/null +++ b/tests/status.up.test.js @@ -0,0 +1,194 @@ +/** + * @file Tests for the status/up cloud function. + */ +import { jest } from "@jest/globals"; +import axios from "axios"; +import MockAdapter from "axios-mock-adapter"; +import up, { RATE_LIMIT_SECONDS } from "../api/status/up.js"; + +const mock = new MockAdapter(axios); + +const successData = { + rateLimit: { + remaining: 4986, + }, +}; + +const faker = (query) => { + const req = { + query: { ...query }, + }; + const res = { + setHeader: jest.fn(), + send: jest.fn(), + }; + + return { req, res }; +}; + +const rate_limit_error = { + errors: [ + { + type: "RATE_LIMITED", + }, + ], +}; + +const bad_credentials_error = { + message: "Bad credentials", +}; + +const shields_up = { + schemaVersion: 1, + label: "Public Instance", + isError: true, + message: "up", + color: "brightgreen", +}; +const shields_down = { + schemaVersion: 1, + label: "Public Instance", + isError: true, + message: "down", + color: "red", +}; + +afterEach(() => { + mock.reset(); +}); + +describe("Test /api/status/up", () => { + it("should return `true` if request was successful", async () => { + mock.onPost("https://api.github.com/graphql").replyOnce(200, successData); + + const { req, res } = faker({}, {}); + await up(req, res); + + expect(res.setHeader).toBeCalledWith("Content-Type", "application/json"); + expect(res.send).toBeCalledWith(true); + }); + + it("should return `false` if all PATs are rate limited", async () => { + mock.onPost("https://api.github.com/graphql").reply(200, rate_limit_error); + + const { req, res } = faker({}, {}); + await up(req, res); + + expect(res.setHeader).toBeCalledWith("Content-Type", "application/json"); + expect(res.send).toBeCalledWith(false); + }); + + it("should return JSON `true` if request was successful and type='json'", async () => { + mock.onPost("https://api.github.com/graphql").replyOnce(200, successData); + + const { req, res } = faker({ type: "json" }, {}); + await up(req, res); + + expect(res.setHeader).toBeCalledWith("Content-Type", "application/json"); + expect(res.send).toBeCalledWith({ up: true }); + }); + + it("should return JSON `false` if all PATs are rate limited and type='json'", async () => { + mock.onPost("https://api.github.com/graphql").reply(200, rate_limit_error); + + const { req, res } = faker({ type: "json" }, {}); + await up(req, res); + + expect(res.setHeader).toBeCalledWith("Content-Type", "application/json"); + expect(res.send).toBeCalledWith({ up: false }); + }); + + it("should return UP shields.io config if request was successful and type='shields'", async () => { + mock.onPost("https://api.github.com/graphql").replyOnce(200, successData); + + const { req, res } = faker({ type: "shields" }, {}); + await up(req, res); + + expect(res.setHeader).toBeCalledWith("Content-Type", "application/json"); + expect(res.send).toBeCalledWith(shields_up); + }); + + it("should return DOWN shields.io config if all PATs are rate limited and type='shields'", async () => { + mock.onPost("https://api.github.com/graphql").reply(200, rate_limit_error); + + const { req, res } = faker({ type: "shields" }, {}); + await up(req, res); + + expect(res.setHeader).toBeCalledWith("Content-Type", "application/json"); + expect(res.send).toBeCalledWith(shields_down); + }); + + it("should return `true` if the first PAT is rate limited but the second PATs works", async () => { + mock + .onPost("https://api.github.com/graphql") + .replyOnce(200, rate_limit_error) + .onPost("https://api.github.com/graphql") + .replyOnce(200, successData); + + const { req, res } = faker({}, {}); + await up(req, res); + + expect(res.setHeader).toBeCalledWith("Content-Type", "application/json"); + expect(res.send).toBeCalledWith(true); + }); + + it("should return `true` if the first PAT has 'Bad credentials' but the second PAT works", async () => { + mock + .onPost("https://api.github.com/graphql") + .replyOnce(404, bad_credentials_error) + .onPost("https://api.github.com/graphql") + .replyOnce(200, successData); + + const { req, res } = faker({}, {}); + await up(req, res); + + expect(res.setHeader).toBeCalledWith("Content-Type", "application/json"); + expect(res.send).toBeCalledWith(true); + }); + + it("should return `false` if all pats have 'Bad credentials'", async () => { + mock + .onPost("https://api.github.com/graphql") + .reply(404, bad_credentials_error); + + const { req, res } = faker({}, {}); + await up(req, res); + + expect(res.setHeader).toBeCalledWith("Content-Type", "application/json"); + expect(res.send).toBeCalledWith(false); + }); + + it("should throw an error if the request fails", async () => { + mock.onPost("https://api.github.com/graphql").networkError(); + + const { req, res } = faker({}, {}); + await up(req, res); + + expect(res.setHeader).toBeCalledWith("Content-Type", "application/json"); + expect(res.send).toBeCalledWith(false); + }); + + it("should have proper cache when no error is thrown", async () => { + mock.onPost("https://api.github.com/graphql").replyOnce(200, successData); + + const { req, res } = faker({}, {}); + await up(req, res); + + expect(res.setHeader.mock.calls).toEqual([ + ["Content-Type", "application/json"], + ["Cache-Control", `max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`], + ]); + }); + + it("should have proper cache when error is thrown", async () => { + mock.onPost("https://api.github.com/graphql").networkError(); + + const { req, res } = faker({}, {}); + await up(req, res); + + expect(res.setHeader.mock.calls).toEqual([ + ["Content-Type", "application/json"], + ["Cache-Control", "no-store"], + ]); + }); +}); diff --git a/themes/README.md b/themes/README.md index b8649d43b95644..5993b6a07a9b8b 100644 --- a/themes/README.md +++ b/themes/README.md @@ -32,10 +32,11 @@ Use `?theme=THEME_NAME` parameter like so :- | `jolly` ![jolly][jolly] | `maroongold` ![maroongold][maroongold] | `yeblu` ![yeblu][yeblu] | | `blueberry` ![blueberry][blueberry] | `slateorange` ![slateorange][slateorange] | `kacho_ga` ![kacho_ga][kacho_ga] | | `outrun` ![outrun][outrun] | `ocean_dark` ![ocean_dark][ocean_dark] | `city_lights` ![city_lights][city_lights] | -| `github_dark` ![github_dark][github_dark] | `discord_old_blurple` ![discord_old_blurple][discord_old_blurple] | `aura_dark` ![aura_dark][aura_dark] | +| `github_dark` ![github_dark][github_dark] | `github_dark_dimmed` ![github_dark_dimmed][github_dark_dimmed] | `discord_old_blurple` ![discord_old_blurple][discord_old_blurple] | | `panda` ![panda][panda] | `noctis_minimus` ![noctis_minimus][noctis_minimus] | `cobalt2` ![cobalt2][cobalt2] | | `swift` ![swift][swift] | `aura` ![aura][aura] | `apprentice` ![apprentice][apprentice] | | `moltack` ![moltack][moltack] | `codeSTACKr` ![codeSTACKr][codeSTACKr] | `rose_pine` ![rose_pine][rose_pine] | +| `aura_dark` ![aura_dark][aura_dark] | | | | [Add your theme][add-theme] | | | ## Repo Card @@ -60,10 +61,11 @@ Use `?theme=THEME_NAME` parameter like so :- | `jolly` ![jolly][jolly_repo] | `maroongold` ![maroongold][maroongold_repo] | `yeblu` ![yeblu][yeblu_repo] | | `blueberry` ![blueberry][blueberry_repo] | `slateorange` ![slateorange][slateorange_repo] | `kacho_ga` ![kacho_ga][kacho_ga_repo] | | `outrun` ![outrun][outrun_repo] | `ocean_dark` ![ocean_dark][ocean_dark_repo] | `city_lights` ![city_lights][city_lights_repo] | -| `github_dark` ![github_dark][github_dark_repo] | `discord_old_blurple` ![discord_old_blurple][discord_old_blurple_repo] | `aura_dark` ![aura_dark][aura_dark_repo] | +| `github_dark` ![github_dark][github_dark_repo] | `github_dark_dimmed` ![github_dark_dimmed][github_dark_dimmed_repo] | `discord_old_blurple` ![discord_old_blurple][discord_old_blurple_repo] | | `panda` ![panda][panda_repo] | `noctis_minimus` ![noctis_minimus][noctis_minimus_repo] | `cobalt2` ![cobalt2][cobalt2_repo] | | `swift` ![swift][swift_repo] | `aura` ![aura][aura_repo] | `apprentice` ![apprentice][apprentice_repo] | | `moltack` ![moltack][moltack_repo] | `codeSTACKr` ![codeSTACKr][codeSTACKr_repo] | `rose_pine` ![rose_pine][rose_pine_repo] | +| `aura_dark` ![aura_dark][aura_dark_repo] | | | | [Add your theme][add-theme] | | | @@ -117,8 +119,8 @@ Use `?theme=THEME_NAME` parameter like so :- [ocean_dark]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=ocean_dark [city_lights]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=city_lights [github_dark]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=github_dark +[github_dark_dimmed]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=github_dark_dimmed [discord_old_blurple]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=discord_old_blurple -[aura_dark]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=aura_dark [panda]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=panda [noctis_minimus]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=noctis_minimus [cobalt2]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=cobalt2 @@ -128,7 +130,7 @@ Use `?theme=THEME_NAME` parameter like so :- [moltack]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=moltack [codeSTACKr]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=codeSTACKr [rose_pine]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=rose_pine - +[aura_dark]: https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=aura_dark [default_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=default [default_repocard_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=default_repocard @@ -180,8 +182,8 @@ Use `?theme=THEME_NAME` parameter like so :- [ocean_dark_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=ocean_dark [city_lights_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=city_lights [github_dark_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=github_dark +[github_dark_dimmed_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=github_dark_dimmed [discord_old_blurple_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=discord_old_blurple -[aura_dark_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=aura_dark [panda_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=panda [noctis_minimus_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=noctis_minimus [cobalt2_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=cobalt2 @@ -191,7 +193,7 @@ Use `?theme=THEME_NAME` parameter like so :- [moltack_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=moltack [codeSTACKr_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=codeSTACKr [rose_pine_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=rose_pine - +[aura_dark_repo]: https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats&cache_seconds=86400&theme=aura_dark [add-theme]: https://github.com/anuraghazra/github-readme-stats/edit/master/themes/index.js diff --git a/themes/index.js b/themes/index.js index a5d3abae8cb6f0..ab8eab6a0d0e92 100644 --- a/themes/index.js +++ b/themes/index.js @@ -18,6 +18,27 @@ export const themes = { text_color: "417E87", bg_color: "ffffff00", }, + shadow_red: { + title_color: "9A0000", + text_color: "444", + icon_color: "4F0000", + border_color: "4F0000", + bg_color: "ffffff00", + }, + shadow_green: { + title_color: "007A00", + text_color: "444", + icon_color: "003D00", + border_color: "003D00", + bg_color: "ffffff00", + }, + shadow_blue: { + title_color: "00779A", + text_color: "444", + icon_color: "004450", + border_color: "004490", + bg_color: "ffffff00", + }, dark: { title_color: "fff", icon_color: "79ff97", @@ -300,6 +321,13 @@ export const themes = { text_color: "C3D1D9", bg_color: "0D1117", }, + github_dark_dimmed: { + title_color: "539bf5", + icon_color: "539bf5", + text_color: "ADBAC7", + bg_color: "24292F", + border_color: "373E47", + }, discord_old_blurple: { title_color: "7289DA", icon_color: "7289DA", @@ -374,6 +402,27 @@ export const themes = { border_color: "170F0C", bg_color: "170F0C", }, + one_dark_pro: { + title_color: "61AFEF", + text_color: "E5C06E", + icon_color: "C678DD", + border_color: "3B4048", + bg_color: "23272E", + }, + rose: { + title_color: "8d192b", + text_color: "862931", + icon_color: "B71F36", + border_color: "e9d8d4", + bg_color: "e9d8d4", + }, + holi: { + title_color: "5FABEE", + text_color: "D6E7FF", + icon_color: "5FABEE", + border_color: "85A4C0", + bg_color: "030314", + }, }; export default themes;