├── LICENSE ├── api ├── index.js ├── pin.js ├── top-langs.js └── wakatime.js ├── codecov.yml ├── jest.config.js ├── package.json ├── powered-by-vercel.svg ├── readme.md ├── scripts ├── generate-theme-doc.js ├── preview-theme.js └── push-theme-readme.sh ├── src ├── calculateRank.js ├── cards │ ├── repo-card.js │ ├── stats-card.js │ ├── top-languages-card.js │ └── wakatime-card.js ├── common │ ├── Card.js │ ├── I18n.js │ ├── blacklist.js │ ├── createProgressNode.js │ ├── icons.js │ ├── languageColors.json │ ├── retryer.js │ └── utils.js ├── fetchers │ ├── repo-fetcher.js │ ├── stats-fetcher.js │ ├── top-languages-fetcher.js │ └── wakatime-fetcher.js ├── getStyles.js └── translations.js ├── tests ├── __snapshots__ │ └── renderWakatimeCard.test.js.snap ├── api.test.js ├── calculateRank.test.js ├── card.test.js ├── fetchRepo.test.js ├── fetchStats.test.js ├── fetchTopLanguages.test.js ├── fetchWakatime.test.js ├── pin.test.js ├── renderRepoCard.test.js ├── renderStatsCard.test.js ├── renderTopLanguages.test.js ├── renderWakatimeCard.test.js ├── retryer.test.js ├── top-langs.test.js └── utils.test.js ├── themes ├── README.md └── index.js └── vercel.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SrGobi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const { 3 | renderError, 4 | parseBoolean, 5 | parseArray, 6 | clampValue, 7 | CONSTANTS, 8 | } = require("../src/common/utils"); 9 | const fetchStats = require("../src/fetchers/stats-fetcher"); 10 | const renderStatsCard = require("../src/cards/stats-card"); 11 | const blacklist = require("../src/common/blacklist"); 12 | const { isLocaleAvailable } = require("../src/translations"); 13 | 14 | module.exports = async (req, res) => { 15 | const { 16 | username, 17 | hide, 18 | hide_title, 19 | hide_border, 20 | hide_rank, 21 | show_icons, 22 | count_private, 23 | include_all_commits, 24 | line_height, 25 | title_color, 26 | icon_color, 27 | text_color, 28 | bg_color, 29 | theme, 30 | cache_seconds, 31 | custom_title, 32 | locale, 33 | disable_animations, 34 | } = req.query; 35 | let stats; 36 | 37 | res.setHeader("Content-Type", "image/svg+xml"); 38 | 39 | if (blacklist.includes(username)) { 40 | return res.send(renderError("Something went wrong")); 41 | } 42 | 43 | if (locale && !isLocaleAvailable(locale)) { 44 | return res.send(renderError("Something went wrong", "Language not found")); 45 | } 46 | 47 | try { 48 | stats = await fetchStats( 49 | username, 50 | parseBoolean(count_private), 51 | parseBoolean(include_all_commits), 52 | ); 53 | 54 | const cacheSeconds = clampValue( 55 | parseInt(cache_seconds || CONSTANTS.TWO_HOURS, 10), 56 | CONSTANTS.TWO_HOURS, 57 | CONSTANTS.ONE_DAY, 58 | ); 59 | 60 | res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`); 61 | 62 | return res.send( 63 | renderStatsCard(stats, { 64 | hide: parseArray(hide), 65 | show_icons: parseBoolean(show_icons), 66 | hide_title: parseBoolean(hide_title), 67 | hide_border: parseBoolean(hide_border), 68 | hide_rank: parseBoolean(hide_rank), 69 | include_all_commits: parseBoolean(include_all_commits), 70 | line_height, 71 | title_color, 72 | icon_color, 73 | text_color, 74 | bg_color, 75 | theme, 76 | custom_title, 77 | locale: locale ? locale.toLowerCase() : null, 78 | disable_animations: parseBoolean(disable_animations), 79 | }), 80 | ); 81 | } catch (err) { 82 | return res.send(renderError(err.message, err.secondaryMessage)); 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /api/pin.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const { 3 | renderError, 4 | parseBoolean, 5 | clampValue, 6 | CONSTANTS, 7 | } = require("../src/common/utils"); 8 | const fetchRepo = require("../src/fetchers/repo-fetcher"); 9 | const renderRepoCard = require("../src/cards/repo-card"); 10 | const blacklist = require("../src/common/blacklist"); 11 | const { isLocaleAvailable } = require("../src/translations"); 12 | 13 | module.exports = async (req, res) => { 14 | const { 15 | username, 16 | repo, 17 | hide_border, 18 | title_color, 19 | icon_color, 20 | text_color, 21 | bg_color, 22 | theme, 23 | show_owner, 24 | cache_seconds, 25 | locale, 26 | } = req.query; 27 | 28 | let repoData; 29 | 30 | res.setHeader("Content-Type", "image/svg+xml"); 31 | 32 | if (blacklist.includes(username)) { 33 | return res.send(renderError("Something went wrong")); 34 | } 35 | 36 | if (locale && !isLocaleAvailable(locale)) { 37 | return res.send(renderError("Something went wrong", "Language not found")); 38 | } 39 | 40 | try { 41 | repoData = await fetchRepo(username, repo); 42 | 43 | let cacheSeconds = clampValue( 44 | parseInt(cache_seconds || CONSTANTS.TWO_HOURS, 10), 45 | CONSTANTS.TWO_HOURS, 46 | CONSTANTS.ONE_DAY, 47 | ); 48 | 49 | /* 50 | if star count & fork count is over 1k then we are kFormating the text 51 | and if both are zero we are not showing the stats 52 | so we can just make the cache longer, since there is no need to frequent updates 53 | */ 54 | const stars = repoData.stargazers.totalCount; 55 | const forks = repoData.forkCount; 56 | const isBothOver1K = stars > 1000 && forks > 1000; 57 | const isBothUnder1 = stars < 1 && forks < 1; 58 | if (!cache_seconds && (isBothOver1K || isBothUnder1)) { 59 | cacheSeconds = CONSTANTS.FOUR_HOURS; 60 | } 61 | 62 | res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`); 63 | 64 | return res.send( 65 | renderRepoCard(repoData, { 66 | hide_border, 67 | title_color, 68 | icon_color, 69 | text_color, 70 | bg_color, 71 | theme, 72 | show_owner: parseBoolean(show_owner), 73 | locale: locale ? locale.toLowerCase() : null, 74 | }), 75 | ); 76 | } catch (err) { 77 | return res.send(renderError(err.message, err.secondaryMessage)); 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /api/top-langs.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const { 3 | renderError, 4 | clampValue, 5 | parseBoolean, 6 | parseArray, 7 | CONSTANTS, 8 | } = require("../src/common/utils"); 9 | const fetchTopLanguages = require("../src/fetchers/top-languages-fetcher"); 10 | const renderTopLanguages = require("../src/cards/top-languages-card"); 11 | const blacklist = require("../src/common/blacklist"); 12 | const { isLocaleAvailable } = require("../src/translations"); 13 | 14 | module.exports = async (req, res) => { 15 | const { 16 | username, 17 | hide, 18 | hide_title, 19 | hide_border, 20 | card_width, 21 | title_color, 22 | text_color, 23 | bg_color, 24 | theme, 25 | cache_seconds, 26 | layout, 27 | langs_count, 28 | exclude_repo, 29 | custom_title, 30 | locale, 31 | } = req.query; 32 | let topLangs; 33 | 34 | res.setHeader("Content-Type", "image/svg+xml"); 35 | 36 | if (blacklist.includes(username)) { 37 | return res.send(renderError("Something went wrong")); 38 | } 39 | 40 | if (locale && !isLocaleAvailable(locale)) { 41 | return res.send(renderError("Something went wrong", "Language not found")); 42 | } 43 | 44 | try { 45 | topLangs = await fetchTopLanguages( 46 | username, 47 | langs_count, 48 | parseArray(exclude_repo), 49 | ); 50 | 51 | const cacheSeconds = clampValue( 52 | parseInt(cache_seconds || CONSTANTS.TWO_HOURS, 10), 53 | CONSTANTS.TWO_HOURS, 54 | CONSTANTS.ONE_DAY, 55 | ); 56 | 57 | res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`); 58 | 59 | return res.send( 60 | renderTopLanguages(topLangs, { 61 | custom_title, 62 | hide_title: parseBoolean(hide_title), 63 | hide_border: parseBoolean(hide_border), 64 | card_width: parseInt(card_width, 10), 65 | hide: parseArray(hide), 66 | title_color, 67 | text_color, 68 | bg_color, 69 | theme, 70 | layout, 71 | locale: locale ? locale.toLowerCase() : null, 72 | }), 73 | ); 74 | } catch (err) { 75 | return res.send(renderError(err.message, err.secondaryMessage)); 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /api/wakatime.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const { 3 | renderError, 4 | parseBoolean, 5 | clampValue, 6 | CONSTANTS, 7 | isLocaleAvailable, 8 | } = require("../src/common/utils"); 9 | const { fetchWakatimeStats } = require("../src/fetchers/wakatime-fetcher"); 10 | const wakatimeCard = require("../src/cards/wakatime-card"); 11 | 12 | module.exports = async (req, res) => { 13 | const { 14 | username, 15 | title_color, 16 | icon_color, 17 | hide_border, 18 | line_height, 19 | text_color, 20 | bg_color, 21 | theme, 22 | cache_seconds, 23 | hide_title, 24 | hide_progress, 25 | custom_title, 26 | locale, 27 | layout, 28 | api_domain, 29 | } = req.query; 30 | 31 | res.setHeader("Content-Type", "image/svg+xml"); 32 | 33 | if (locale && !isLocaleAvailable(locale)) { 34 | return res.send(renderError("Something went wrong", "Language not found")); 35 | } 36 | 37 | try { 38 | const stats = await fetchWakatimeStats({ username, api_domain }); 39 | 40 | let cacheSeconds = clampValue( 41 | parseInt(cache_seconds || CONSTANTS.TWO_HOURS, 10), 42 | CONSTANTS.TWO_HOURS, 43 | CONSTANTS.ONE_DAY, 44 | ); 45 | 46 | if (!cache_seconds) { 47 | cacheSeconds = CONSTANTS.FOUR_HOURS; 48 | } 49 | 50 | res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`); 51 | 52 | return res.send( 53 | wakatimeCard(stats, { 54 | custom_title, 55 | hide_title: parseBoolean(hide_title), 56 | hide_border: parseBoolean(hide_border), 57 | line_height, 58 | title_color, 59 | icon_color, 60 | text_color, 61 | bg_color, 62 | theme, 63 | hide_progress, 64 | locale: locale ? locale.toLowerCase() : null, 65 | layout, 66 | }), 67 | ); 68 | } catch (err) { 69 | return res.send(renderError(err.message, err.secondaryMessage)); 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: "70...100" 8 | 9 | status: 10 | project: 11 | default: 12 | threshold: 5 13 | patch: false 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-readme-stats", 3 | "version": "1.0.0", 4 | "description": "Dynamically generate stats for your github readmes", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest --coverage", 8 | "test:watch": "jest --watch", 9 | "theme-readme-gen": "node scripts/generate-theme-doc", 10 | "preview-theme": "node scripts/preview-theme" 11 | }, 12 | "author": "Anurag Hazra", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@actions/core": "^1.2.4", 16 | "@actions/github": "^4.0.0", 17 | "@testing-library/dom": "^7.20.0", 18 | "@testing-library/jest-dom": "^5.11.0", 19 | "axios": "^0.19.2", 20 | "axios-mock-adapter": "^1.18.1", 21 | "css-to-object": "^1.1.0", 22 | "husky": "^4.2.5", 23 | "jest": "^26.1.0", 24 | "parse-diff": "^0.7.0" 25 | }, 26 | "dependencies": { 27 | "dotenv": "^8.2.0", 28 | "emoji-name-map": "^1.2.8", 29 | "github-username-regex": "^1.0.0", 30 | "prettier": "^2.1.2", 31 | "word-wrap": "^1.2.3" 32 | }, 33 | "husky": { 34 | "hooks": { 35 | "pre-commit": "npm test" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /powered-by-vercel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 |

Estadísticas Léame de GitHub

3 |

¡Obtenga estadísticas de GitHub generadas dinámicamente en sus archivos Léame!

4 | 5 | # GitHub Stats Card 6 | 7 | Copie y pegue esto en su contenido de rebajas, y eso es todo. ¡Sencillo! 8 | 9 | Cambiar el `?username=` valor al nombre de usuario de tu GitHub. 10 | 11 | ```md 12 | [![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=SrGobi)](https://github.com/SrGobi/github-readme-stats) 13 | ``` 14 | 15 | _Nota: Los rangos disponibles son S + (1% superior), S (25% superior), A ++ (45% superior), A + (60% superior) y B + (todos). 16 | Los valores se calculan utilizando el [cumulative distribution function](https://en.wikipedia.org/wiki/Cumulative_distribution_function) utilizando confirmaciones, contribuciones, problemas, estrellas, solicitudes de extracción, seguidores y repositorios propios. 17 | La implementación se puede investigar en [src/calculateRank.js](./src/calculateRank.js)_ 18 | 19 | ### Ocultar estadísticas individuales 20 | 21 | Para ocultar estadísticas específicas, puede pasar un parámetro de consulta `?hide=` con valores separados por comas. 22 | 23 | > Opciones: `&hide=stars,commits,prs,issues,contribs` 24 | 25 | ```md 26 | ![SrGobi GitHub stats](https://github-readme-stats.vercel.app/api?username=SrGobi&hide=contribs,prs) 27 | ``` 28 | 29 | ### Agregar el recuento de contribuciones privadas al recuento total de confirmaciones 30 | 31 | Puede agregar el recuento de todas sus contribuciones privadas al recuento total de confirmaciones utilizando el parámetro de consulta `?count_private=true`. 32 | 33 | _Nota: Si está implementando este proyecto usted mismo, las contribuciones privadas se contarán de forma predeterminada; de lo contrario, debe elegir compartir sus recuentos de contribuciones privadas._ 34 | 35 | > Opciones: `&count_private=true` 36 | 37 | ```md 38 | ![SrGobi GitHub stats](https://github-readme-stats.vercel.app/api?username=SrGobi&count_private=true) 39 | ``` 40 | 41 | ### Mostrando iconos 42 | 43 | Para habilitar iconos, puede pasar `show_icons = true` en el parámetro de consulta, así: 44 | 45 | ```md 46 | ![SrGobi GitHub stats](https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true) 47 | ``` 48 | 49 | ### Temas 50 | 51 | Con temas incorporados, puede personalizar el aspecto de la tarjeta sin hacer nada [manual customization](#personalización). 52 | 53 | Usa `?theme=THEME_NAME` parámetro así :- 54 | 55 | ```md 56 | ![SrGobi GitHub stats](https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&theme=radical) 57 | ``` 58 | 59 | #### Todos los temas incorporados :- 60 | 61 | dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula 62 | 63 | GitHub Readme Stats Themes 64 | 65 | Puede ver una vista previa de [all available themes](./themes/README.md) o echa un vistazo al [theme config file](./themes/index.js) & **también puedes contribuir con nuevos temas** si quieres :D 66 | 67 | ### Personalización 68 | 69 | Puede personalizar la apariencia de su `Stats Card` or `Repo Card` como quieras con los parámetros de URL. 70 | 71 | #### Opciones comunes: 72 | 73 | - `title_color` - Color del título de la tarjeta _(hex color)_ 74 | - `text_color` - Color del texto del cuerpo _(hex color)_ 75 | - `icon_color` - Color de los iconos si está disponible _(hex color)_ 76 | - `bg_color` - Color de fondo de la tarjeta _(hex color)_ **o** un degradado en forma de _angle,start,end_ 77 | - `hide_border` - Oculta el borde de la tarjeta _(boolean)_ 78 | - `theme` - nombre del tema, elija entre [all available themes](./themes/README.md) 79 | - `cache_seconds` - configurar el encabezado de la caché manualmente _(min: 1800, max: 86400)_ 80 | - `locale` - configurar el idioma en la tarjeta _(e.g. cn, de, es, etc.)_ 81 | 82 | ##### Degradado en bg_color 83 | 84 | Puede proporcionar varios valores separados por comas en la opción bg_color para representar un degradado, el formato del degradado es :- 85 | 86 | ``` 87 | &bg_color=DEG,COLOR1,COLOR2,COLOR3...COLOR10 88 | ``` 89 | 90 | > Nota sobre el caché: las tarjetas Repo tienen un caché predeterminado de 4 horas (14400 segundos) si el recuento de bifurcaciones y el recuento de estrellas es inferior a 1k; de lo contrario, son 2 horas (7200 segundos). Además, tenga en cuenta que la caché está sujeta a un mínimo de 2 horas y un máximo de 24 horas. 91 | 92 | #### Opciones exclusivas de la tarjeta de estadísticas: 93 | 94 | - `hide` - Oculta los elementos especificados de las estadísticas _(Comma-separated values)_ 95 | - `hide_title` - _(boolean)_ 96 | - `hide_rank` - _(boolean)_ oculta el rango y cambia automáticamente el tamaño del ancho de la tarjeta 97 | - `show_icons` - _(boolean)_ 98 | - `include_all_commits` - Cuente las confirmaciones totales en lugar de solo las confirmaciones del año actual _(boolean)_ 99 | - `count_private` - Contar confirmaciones privadas _(boolean)_ 100 | - `line_height` - Establece la altura de la línea entre el texto. _(number)_ 101 | - `custom_title` - Establece un título personalizado para la tarjeta 102 | - `disable_animations` - Desactiva todas las animaciones de la tarjeta. _(boolean)_ 103 | 104 | #### Opciones exclusivas de la tarjeta Repo: 105 | 106 | - `show_owner` - Muestra el nombre del propietario del repositorio _(boolean)_ 107 | 108 | #### Opciones exclusivas de la tarjeta de idioma: 109 | 110 | - `hide` - Ocultar los idiomas especificados de la tarjeta _(Comma-separated values)_ 111 | - `hide_title` - _(boolean)_ 112 | - `layout` - Cambiar entre dos diseños disponibles `predeterminado` y `compacto` 113 | - `card_width` - Establecer el ancho de la tarjeta manualmente _(number)_ 114 | - `langs_count` - Mostrar más idiomas en la tarjeta, entre 1 y 10, el valor predeterminado es 5 _(number)_ 115 | - `exclude_repo` - Excluir repositorios especificados _(Comma-separated values)_ 116 | - `custom_title` - Establece un título personalizado para la tarjeta 117 | 118 | > :warning: **Importante:** 119 | > Los nombres de los idiomas deben tener un escape de uri, como se especifica en [Percent Encoding](https://en.wikipedia.org/wiki/Percent-encoding) 120 | > (i.e: `c++` debe convertirse `c%2B%2B`, `jupyter notebook` debería convertirse `jupyter%20notebook`, etc.) Puedes usar 121 | > [urlencoder.org](https://www.urlencoder.org/) para ayudarlo a hacer esto automáticamente. 122 | 123 | #### Opciones exclusivas de la tarjeta Wakatime: 124 | 125 | - `hide_title` - _(boolean)_ 126 | - `line_height` - Establece la altura de la línea entre el texto. _(number)_ 127 | - `hide_progress` - Oculta la barra de progreso y el porcentaje _(boolean)_ 128 | - `custom_title` - Establece un título personalizado para la tarjeta 129 | - `layout` - Cambiar entre dos diseños disponibles `predeterminado` y` compacto` 130 | - `api_domain` - Establecer un dominio de API personalizado para la tarjeta 131 | 132 | --- 133 | 134 | # Pines adicionales de GitHub 135 | 136 | Los pines adicionales de GitHub le permiten anclar más de 6 repositorios en su perfil usando un perfil Léame de GitHub. 137 | 138 | ¡Hurra! Ya no está limitado a 6 repositorios anclados. 139 | 140 | ### Uso 141 | 142 | Copie y pegue este código en su archivo Léame y cambie los enlaces. 143 | 144 | Endpoint: `api/pin?username=SrGobi&repo=github-readme-stats` 145 | 146 | ```md 147 | [![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats)](https://github.com/SrGobi/github-readme-stats) 148 | ``` 149 | 150 | ### Demo 151 | 152 | [![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats)](https://github.com/SrGobi/github-readme-stats) 153 | 154 | Use [show_owner](#personalización) variable para incluir el nombre de usuario del propietario del repositorio 155 | 156 | [![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&show_owner=true)](https://github.com/SrGobi/github-readme-stats) 157 | 158 | # Tarjeta de idiomas principales 159 | 160 | La tarjeta de idiomas principales muestra los idiomas principales de un usuario de GitHub que más se han utilizado. 161 | 162 | _NOTA: Top Languages no indica mi nivel de habilidad ni nada por el estilo, es una métrica de GitHub de qué idiomas tienen más código en GitHub. Es una nueva característica de github-readme-stats._ 163 | 164 | ### Uso 165 | 166 | Copie y pegue este código en su archivo Léame y cambie los enlaces. 167 | 168 | Endpoint: `api/top-langs?username=SrGobi` 169 | 170 | ```md 171 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=SrGobi)](https://github.com/SrGobi/github-readme-stats) 172 | ``` 173 | 174 | ### Excluir repositorios individuales 175 | 176 | Puedes usar `?exclude_repo=repo1,repo2` parámetro para excluir repositorios individuales. 177 | 178 | ```md 179 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=SrGobi&exclude_repo=github-readme-stats,SrGobi.github.io)](https://github.com/SrGobi/github-readme-stats) 180 | ``` 181 | 182 | ### Hide individual languages 183 | 184 | You can use `?hide=language1,language2` parameter to hide individual languages. 185 | 186 | ```md 187 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=SrGobi&hide=javascript,html)](https://github.com/SrGobi/github-readme-stats) 188 | ``` 189 | 190 | ### Mostrar más idiomas 191 | 192 | Puedes usar el `&langs_count=` opción para aumentar o disminuir el número de idiomas que se muestran en la tarjeta. Los valores válidos son números enteros entre 1 y 10 (inclusive) y el valor predeterminado es 5. 193 | 194 | ```md 195 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=SrGobi&langs_count=8)](https://github.com/SrGobi/github-readme-stats) 196 | ``` 197 | 198 | ### Diseño de tarjeta de idioma compacto 199 | 200 | Puede utilizar la opción `& layout = compact` para cambiar el diseño de la tarjeta. 201 | 202 | ```md 203 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=SrGobi&layout=compact)](https://github.com/SrGobi/github-readme-stats) 204 | ``` 205 | 206 | ### Demo 207 | 208 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=SrGobi)](https://github.com/SrGobi/github-readme-stats) 209 | 210 | - Disposición compacta 211 | 212 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=SrGobi&layout=compact)](https://github.com/SrGobi/github-readme-stats) 213 | 214 | # Estadísticas de la semana de Wakatime 215 | 216 | Cambie el valor `? Username =` por su [Wakatime](https://wakatime.com) username. 217 | 218 | ```md 219 | [![SrGobi's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=srgobi)](https://github.com/SrGobi/github-readme-stats) 220 | ``` 221 | 222 | ### Demo 223 | 224 | [![SrGobi's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=srgobi)](https://github.com/SrGobi/github-readme-stats) 225 | 226 | [![SrGobi's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=srgobi&hide_progress=true)](https://github.com/SrGobi/github-readme-stats) 227 | 228 | - Disposición compacta 229 | 230 | [![SrGobi's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=srgobi&layout=compact)](https://github.com/SrGobi/github-readme-stats) 231 | 232 | --- 233 | 234 | ### Todas las demostraciones 235 | 236 | - Defecto 237 | 238 | ![SrGobi's GitHub stats](https://github-readme-stats.vercel.app/api?username=SrGobi) 239 | 240 | - Ocultar estadísticas específicas 241 | 242 | ![SrGobi's GitHub stats](https://github-readme-stats.vercel.app/api?username=SrGobi&hide=contribs,issues) 243 | 244 | - Mostrando iconos 245 | 246 | ![SrGobi's GitHub stats](https://github-readme-stats.vercel.app/api?username=SrGobi&hide=issues&show_icons=true) 247 | 248 | - Incluir todas las confirmaciones 249 | 250 | ![SrGobi's GitHub stats](https://github-readme-stats.vercel.app/api?username=SrGobi&include_all_commits=true) 251 | 252 | - Temas 253 | 254 | Elija entre cualquiera de los [default themes](#temas) 255 | 256 | ![SrGobi's GitHub stats](https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&theme=radical) 257 | 258 | - Degradado 259 | 260 | ![SrGobi's GitHub stats](https://github-readme-stats.vercel.app/api?username=SrGobi&bg_color=30,e96443,904e95&title_color=fff&text_color=fff) 261 | 262 | - Personalizar la tarjeta de estadísticas 263 | 264 | ![SrGobi's GitHub stats](https://github-readme-stats.vercel.app/api/?username=SrGobi&show_icons=true&title_color=fff&icon_color=79ff97&text_color=9f9f9f&bg_color=151515) 265 | 266 | - Configuración de la configuración regional de la tarjeta 267 | 268 | ![SrGobi's GitHub stats](https://github-readme-stats.vercel.app/api/?username=SrGobi&locale=es) 269 | 270 | - Personalización de la tarjeta de repositorio 271 | 272 | ![Customized Card](https://github-readme-stats.vercel.app/api/pin?username=SrGobi&repo=github-readme-stats&title_color=fff&icon_color=f9f9f9&text_color=9f9f9f&bg_color=151515) 273 | 274 | - Idiomas principales 275 | 276 | [![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=SrGobi)](https://github.com/SrGobi/github-readme-stats) 277 | 278 | - Tarjeta de Wakatime 279 | 280 | [![SrGobi wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=srgobi)](https://github.com/SrGobi/github-readme-stats) 281 | 282 | --- 283 | 284 | ### Sugerencia rápida (alinear las tarjetas de repositorio) 285 | 286 | Por lo general, no podrá diseñar las imágenes una al lado de la otra. Para hacer eso, puede utilizar este enfoque: 287 | 288 | ```md 289 | 290 | 291 | 292 | 293 | 294 | 295 | ``` 296 | 297 | ## :sparkling_heart: Apoya el proyecto 298 | 299 | Utilizo código abierto en casi todo lo que puedo y trato de responder a todos los que necesitan ayuda para usar estos proyectos. Obviamente, 300 | esto lleva tiempo. Puede utilizar este servicio de forma gratuita. 301 | 302 | Sin embargo, si está utilizando este proyecto y está satisfecho con él o simplemente quiere animarme a seguir creando cosas, hay algunas formas de hacerlo. :- 303 | 304 | - Dar el crédito adecuado cuando usa github-readme-stats en su archivo Léame, vinculándolo de nuevo :D 305 | - Protagonizar y compartir el proyecto :rocket: 306 | 307 | Gracias! :heart: 308 | 309 | --- 310 | 311 | Las contribuciones son bienvenidas! <3 312 | 313 | Hecho con :heart: y JavaScript. 314 | -------------------------------------------------------------------------------- /scripts/generate-theme-doc.js: -------------------------------------------------------------------------------- 1 | const theme = require("../themes/index"); 2 | const fs = require("fs"); 3 | 4 | const TARGET_FILE = "./themes/README.md"; 5 | const REPO_CARD_LINKS_FLAG = ""; 6 | const STAT_CARD_LINKS_FLAG = ""; 7 | 8 | const STAT_CARD_TABLE_FLAG = ""; 9 | const REPO_CARD_TABLE_FLAG = ""; 10 | 11 | const THEME_TEMPLATE = `## Available Themes 12 | 13 | 14 | 15 | With inbuilt themes you can customize the look of the card without doing any manual customization. 16 | 17 | Use \`?theme=THEME_NAME\` parameter like so :- 18 | 19 | \`\`\`md 20 | ![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=SrGobi&theme=dark&show_icons=true) 21 | \`\`\` 22 | 23 | ## Stats 24 | 25 | > These themes work both for the Stats Card and Repo Card. 26 | 27 | | | | | 28 | | :--: | :--: | :--: | 29 | ${STAT_CARD_TABLE_FLAG} 30 | 31 | ## Repo Card 32 | 33 | > These themes work both for the Stats Card and Repo Card. 34 | 35 | | | | | 36 | | :--: | :--: | :--: | 37 | ${REPO_CARD_TABLE_FLAG} 38 | 39 | ${STAT_CARD_LINKS_FLAG} 40 | 41 | ${REPO_CARD_LINKS_FLAG} 42 | 43 | 44 | [add-theme]: https://github.com/SrGobi/github-readme-stats/edit/master/themes/index.js 45 | 46 | Wanted to add a new theme? Consider reading the [contribution guidelines](../CONTRIBUTING.md#themes-contribution) :D 47 | `; 48 | 49 | const createRepoMdLink = (theme) => { 50 | return `\n[${theme}_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=${theme}`; 51 | }; 52 | const createStatMdLink = (theme) => { 53 | return `\n[${theme}]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=${theme}`; 54 | }; 55 | 56 | const generateLinks = (fn) => { 57 | return Object.keys(theme) 58 | .map((name) => fn(name)) 59 | .join(""); 60 | }; 61 | 62 | const createTableItem = ({ link, label, isRepoCard }) => { 63 | if (!link || !label) return ""; 64 | return `\`${label}\` ![${link}][${link}${isRepoCard ? "_repo" : ""}]`; 65 | }; 66 | const generateTable = ({ isRepoCard }) => { 67 | const rows = []; 68 | const themes = Object.keys(theme).filter( 69 | (name) => name !== (!isRepoCard ? "default_repocard" : "default"), 70 | ); 71 | 72 | for (let i = 0; i < themes.length; i += 3) { 73 | const one = themes[i]; 74 | const two = themes[i + 1]; 75 | const three = themes[i + 2]; 76 | 77 | let tableItem1 = createTableItem({ link: one, label: one, isRepoCard }); 78 | let tableItem2 = createTableItem({ link: two, label: two, isRepoCard }); 79 | let tableItem3 = createTableItem({ link: three, label: three, isRepoCard }); 80 | 81 | if (three === undefined) { 82 | tableItem3 = `[Add your theme][add-theme]`; 83 | } 84 | rows.push(`| ${tableItem1} | ${tableItem2} | ${tableItem3} |`); 85 | 86 | // if it's the last row & the row has no empty space push a new row 87 | if (three && i + 3 === themes.length) { 88 | rows.push(`| [Add your theme][add-theme] | | |`); 89 | } 90 | } 91 | 92 | return rows.join("\n"); 93 | }; 94 | 95 | const buildReadme = () => { 96 | return THEME_TEMPLATE.split("\n") 97 | .map((line) => { 98 | if (line.includes(REPO_CARD_LINKS_FLAG)) { 99 | return generateLinks(createRepoMdLink); 100 | } 101 | if (line.includes(STAT_CARD_LINKS_FLAG)) { 102 | return generateLinks(createStatMdLink); 103 | } 104 | if (line.includes(REPO_CARD_TABLE_FLAG)) { 105 | return generateTable({ isRepoCard: true }); 106 | } 107 | if (line.includes(STAT_CARD_TABLE_FLAG)) { 108 | return generateTable({ isRepoCard: false }); 109 | } 110 | return line; 111 | }) 112 | .join("\n"); 113 | }; 114 | 115 | fs.writeFileSync(TARGET_FILE, buildReadme()); 116 | -------------------------------------------------------------------------------- /scripts/preview-theme.js: -------------------------------------------------------------------------------- 1 | const core = require("@actions/core"); 2 | const github = require("@actions/github"); 3 | const parse = require("parse-diff"); 4 | require("dotenv").config(); 5 | 6 | function getPrNumber() { 7 | const pullRequest = github.context.payload.pull_request; 8 | if (!pullRequest) { 9 | return undefined; 10 | } 11 | 12 | return pullRequest.number; 13 | } 14 | 15 | const themeContribGuidelines = ` 16 | \r> Hi thanks for the theme contribution, please read our theme contribution guidelines 17 | 18 | \r> We are currently only accepting color combinations from any vscode theme or which has good color combination to minimize bloating the themes collection. 19 | 20 | \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/SrGobi/github-readme-stats#customization) 21 | \r> Read our [contribution guidelines](https://github.com/SrGobi/github-readme-stats/blob/master/CONTRIBUTING.md) for more info 22 | `; 23 | 24 | async function run() { 25 | try { 26 | const token = core.getInput("token"); 27 | const octokit = github.getOctokit(token || process.env.PERSONAL_TOKEN); 28 | const pullRequestId = getPrNumber(); 29 | 30 | if (!pullRequestId) { 31 | console.log("PR not found"); 32 | return; 33 | } 34 | 35 | let res = await octokit.pulls.get({ 36 | owner: "SrGobi", 37 | repo: "github-readme-stats", 38 | pull_number: pullRequestId, 39 | mediaType: { 40 | format: "diff", 41 | }, 42 | }); 43 | 44 | let diff = parse(res.data); 45 | let colorStrings = diff 46 | .find((file) => file.to === "themes/index.js") 47 | .chunks[0].changes.filter((c) => c.type === "add") 48 | .map((c) => c.content.replace("+", "")) 49 | .join(""); 50 | 51 | let matches = colorStrings.match(/(title_color:.*bg_color.*\")/); 52 | let colors = matches && matches[0].split(","); 53 | 54 | if (!colors) { 55 | await octokit.issues.createComment({ 56 | owner: "SrGobi", 57 | repo: "github-readme-stats", 58 | body: ` 59 | \rTheme preview (bot) 60 | 61 | \rCannot create theme preview 62 | 63 | ${themeContribGuidelines} 64 | `, 65 | issue_number: pullRequestId, 66 | }); 67 | return; 68 | } 69 | colors = colors.map((color) => 70 | color.replace(/.*\:\s/, "").replace(/\"/g, ""), 71 | ); 72 | 73 | const titleColor = colors[0]; 74 | const iconColor = colors[1]; 75 | const textColor = colors[2]; 76 | const bgColor = colors[3]; 77 | const url = `https://github-readme-stats.vercel.app/api?username=SrGobi&title_color=${titleColor}&icon_color=${iconColor}&text_color=${textColor}&bg_color=${bgColor}&show_icons=true`; 78 | 79 | await octokit.issues.createComment({ 80 | owner: "SrGobi", 81 | repo: "github-readme-stats", 82 | body: ` 83 | \rTheme preview (bot) 84 | 85 | \ntitle_color: #${titleColor} | icon_color: #${iconColor} | text_color: #${textColor} | bg_color: #${bgColor} 86 | 87 | \rLink: ${url} 88 | 89 | \r[![](${url})](${url}) 90 | 91 | ${themeContribGuidelines} 92 | `, 93 | issue_number: pullRequestId, 94 | }); 95 | } catch (error) { 96 | console.log(error); 97 | } 98 | } 99 | 100 | run(); 101 | -------------------------------------------------------------------------------- /scripts/push-theme-readme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export BRANCH_NAME=updated-theme-readme 6 | git --version 7 | git config --global user.email "no-reply@githubreadmestats.com" 8 | git config --global user.name "Github Readme Stats Bot" 9 | git branch -d $BRANCH_NAME || true 10 | git checkout -b $BRANCH_NAME 11 | git add --all 12 | git commit --message "docs(theme): Auto update theme readme" || exit 0 13 | git remote add origin-$BRANCH_NAME https://${PERSONAL_TOKEN}@github.com/${GH_REPO}.git 14 | git push --force --quiet --set-upstream origin-$BRANCH_NAME $BRANCH_NAME -------------------------------------------------------------------------------- /src/calculateRank.js: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/a/5263759/10629172 2 | function normalcdf(mean, sigma, to) { 3 | var z = (to - mean) / Math.sqrt(2 * sigma * sigma); 4 | var t = 1 / (1 + 0.3275911 * Math.abs(z)); 5 | var a1 = 0.254829592; 6 | var a2 = -0.284496736; 7 | var a3 = 1.421413741; 8 | var a4 = -1.453152027; 9 | var a5 = 1.061405429; 10 | var erf = 11 | 1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-z * z); 12 | var sign = 1; 13 | if (z < 0) { 14 | sign = -1; 15 | } 16 | return (1 / 2) * (1 + sign * erf); 17 | } 18 | 19 | function calculateRank({ 20 | totalRepos, 21 | totalCommits, 22 | contributions, 23 | followers, 24 | prs, 25 | issues, 26 | stargazers, 27 | }) { 28 | const COMMITS_OFFSET = 1.65; 29 | const CONTRIBS_OFFSET = 1.65; 30 | const ISSUES_OFFSET = 1; 31 | const STARS_OFFSET = 0.75; 32 | const PRS_OFFSET = 0.5; 33 | const FOLLOWERS_OFFSET = 0.45; 34 | const REPO_OFFSET = 1; 35 | 36 | const ALL_OFFSETS = 37 | CONTRIBS_OFFSET + 38 | ISSUES_OFFSET + 39 | STARS_OFFSET + 40 | PRS_OFFSET + 41 | FOLLOWERS_OFFSET + 42 | REPO_OFFSET; 43 | 44 | const RANK_S_VALUE = 1; 45 | const RANK_DOUBLE_A_VALUE = 25; 46 | const RANK_A2_VALUE = 45; 47 | const RANK_A3_VALUE = 60; 48 | const RANK_B_VALUE = 100; 49 | 50 | const TOTAL_VALUES = 51 | RANK_S_VALUE + RANK_A2_VALUE + RANK_A3_VALUE + RANK_B_VALUE; 52 | 53 | // prettier-ignore 54 | const score = ( 55 | totalCommits * COMMITS_OFFSET + 56 | contributions * CONTRIBS_OFFSET + 57 | issues * ISSUES_OFFSET + 58 | stargazers * STARS_OFFSET + 59 | prs * PRS_OFFSET + 60 | followers * FOLLOWERS_OFFSET + 61 | totalRepos * REPO_OFFSET 62 | ) / 100; 63 | 64 | const normalizedScore = normalcdf(score, TOTAL_VALUES, ALL_OFFSETS) * 100; 65 | 66 | let level = ""; 67 | 68 | if (normalizedScore < RANK_S_VALUE) { 69 | level = "S+"; 70 | } 71 | if ( 72 | normalizedScore >= RANK_S_VALUE && 73 | normalizedScore < RANK_DOUBLE_A_VALUE 74 | ) { 75 | level = "S"; 76 | } 77 | if ( 78 | normalizedScore >= RANK_DOUBLE_A_VALUE && 79 | normalizedScore < RANK_A2_VALUE 80 | ) { 81 | level = "A++"; 82 | } 83 | if (normalizedScore >= RANK_A2_VALUE && normalizedScore < RANK_A3_VALUE) { 84 | level = "A+"; 85 | } 86 | if (normalizedScore >= RANK_A3_VALUE && normalizedScore < RANK_B_VALUE) { 87 | level = "B+"; 88 | } 89 | 90 | return { level, score: normalizedScore }; 91 | } 92 | 93 | module.exports = calculateRank; 94 | -------------------------------------------------------------------------------- /src/cards/repo-card.js: -------------------------------------------------------------------------------- 1 | const toEmoji = require("emoji-name-map"); 2 | const { 3 | kFormatter, 4 | encodeHTML, 5 | getCardColors, 6 | FlexLayout, 7 | wrapTextMultiline, 8 | } = require("../common/utils"); 9 | const I18n = require("../common/I18n"); 10 | const Card = require("../common/Card"); 11 | const icons = require("../common/icons"); 12 | const { repoCardLocales } = require("../translations"); 13 | 14 | const renderRepoCard = (repo, options = {}) => { 15 | const { 16 | name, 17 | nameWithOwner, 18 | description, 19 | primaryLanguage, 20 | stargazers, 21 | isArchived, 22 | isTemplate, 23 | forkCount, 24 | } = repo; 25 | const { 26 | hide_border = false, 27 | title_color, 28 | icon_color, 29 | text_color, 30 | bg_color, 31 | show_owner, 32 | theme = "default_repocard", 33 | locale, 34 | } = options; 35 | 36 | const header = show_owner ? nameWithOwner : name; 37 | const langName = (primaryLanguage && primaryLanguage.name) || "Unspecified"; 38 | const langColor = (primaryLanguage && primaryLanguage.color) || "#333"; 39 | 40 | const shiftText = langName.length > 15 ? 0 : 30; 41 | 42 | let desc = description || "No description provided"; 43 | 44 | // parse emojis to unicode 45 | desc = desc.replace(/:\w+:/gm, (emoji) => { 46 | return toEmoji.get(emoji) || ""; 47 | }); 48 | 49 | const multiLineDescription = wrapTextMultiline(desc); 50 | const descriptionLines = multiLineDescription.length; 51 | const lineHeight = 10; 52 | 53 | const height = 54 | (descriptionLines > 1 ? 120 : 110) + descriptionLines * lineHeight; 55 | 56 | const i18n = new I18n({ 57 | locale, 58 | translations: repoCardLocales, 59 | }); 60 | 61 | // returns theme based colors with proper overrides and defaults 62 | const { titleColor, textColor, iconColor, bgColor } = getCardColors({ 63 | title_color, 64 | icon_color, 65 | text_color, 66 | bg_color, 67 | theme, 68 | }); 69 | 70 | const totalStars = kFormatter(stargazers.totalCount); 71 | const totalForks = kFormatter(forkCount); 72 | 73 | const getBadgeSVG = (label) => ` 74 | 75 | 76 | 83 | ${label} 84 | 85 | 86 | `; 87 | 88 | const svgLanguage = primaryLanguage 89 | ? ` 90 | 91 | 92 | ${langName} 93 | 94 | ` 95 | : ""; 96 | 97 | const iconWithLabel = (icon, label, testid) => { 98 | return ` 99 | 100 | ${icon} 101 | 102 | ${label} 103 | `; 104 | }; 105 | const svgStars = 106 | stargazers.totalCount > 0 && 107 | iconWithLabel(icons.star, totalStars, "stargazers"); 108 | const svgForks = 109 | forkCount > 0 && iconWithLabel(icons.fork, totalForks, "forkcount"); 110 | 111 | const starAndForkCount = FlexLayout({ 112 | items: [svgStars, svgForks], 113 | gap: 65, 114 | }).join(""); 115 | 116 | const card = new Card({ 117 | defaultTitle: header, 118 | titlePrefixIcon: icons.contribs, 119 | width: 400, 120 | height, 121 | colors: { 122 | titleColor, 123 | textColor, 124 | iconColor, 125 | bgColor, 126 | }, 127 | }); 128 | 129 | card.disableAnimations(); 130 | card.setHideBorder(hide_border); 131 | card.setHideTitle(false); 132 | card.setCSS(` 133 | .description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} } 134 | .gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} } 135 | .icon { fill: ${iconColor} } 136 | .badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; } 137 | .badge rect { opacity: 0.2 } 138 | `); 139 | 140 | return card.render(` 141 | ${ 142 | isTemplate 143 | ? getBadgeSVG(i18n.t("repocard.template")) 144 | : isArchived 145 | ? getBadgeSVG(i18n.t("repocard.archived")) 146 | : "" 147 | } 148 | 149 | 150 | ${multiLineDescription 151 | .map((line) => `${encodeHTML(line)}`) 152 | .join("")} 153 | 154 | 155 | 156 | ${svgLanguage} 157 | 158 | 162 | ${starAndForkCount} 163 | 164 | 165 | `); 166 | }; 167 | 168 | module.exports = renderRepoCard; 169 | -------------------------------------------------------------------------------- /src/cards/stats-card.js: -------------------------------------------------------------------------------- 1 | const I18n = require("../common/I18n"); 2 | const Card = require("../common/Card"); 3 | const icons = require("../common/icons"); 4 | const { getStyles } = require("../getStyles"); 5 | const { statCardLocales } = require("../translations"); 6 | const { 7 | kFormatter, 8 | FlexLayout, 9 | clampValue, 10 | measureText, 11 | getCardColors, 12 | } = require("../common/utils"); 13 | 14 | const createTextNode = ({ 15 | icon, 16 | label, 17 | value, 18 | id, 19 | index, 20 | showIcons, 21 | shiftValuePos, 22 | }) => { 23 | const kValue = kFormatter(value); 24 | const staggerDelay = (index + 3) * 150; 25 | 26 | const labelOffset = showIcons ? `x="25"` : ""; 27 | const iconSvg = showIcons 28 | ? ` 29 | 30 | ${icon} 31 | 32 | ` 33 | : ""; 34 | return ` 35 | 36 | ${iconSvg} 37 | ${label}: 38 | ${kValue} 44 | 45 | `; 46 | }; 47 | 48 | const renderStatsCard = (stats = {}, options = { hide: [] }) => { 49 | const { 50 | name, 51 | totalStars, 52 | totalCommits, 53 | totalIssues, 54 | totalPRs, 55 | contributedTo, 56 | rank, 57 | } = stats; 58 | const { 59 | hide = [], 60 | show_icons = false, 61 | hide_title = false, 62 | hide_border = false, 63 | hide_rank = false, 64 | include_all_commits = false, 65 | line_height = 25, 66 | title_color, 67 | icon_color, 68 | text_color, 69 | bg_color, 70 | theme = "default", 71 | custom_title, 72 | locale, 73 | disable_animations = false, 74 | } = options; 75 | 76 | const lheight = parseInt(line_height, 10); 77 | 78 | // returns theme based colors with proper overrides and defaults 79 | const { titleColor, textColor, iconColor, bgColor } = getCardColors({ 80 | title_color, 81 | icon_color, 82 | text_color, 83 | bg_color, 84 | theme, 85 | }); 86 | 87 | const apostrophe = ["x", "s"].includes(name.slice(-1).toLocaleLowerCase()) 88 | ? "" 89 | : "s"; 90 | const i18n = new I18n({ 91 | locale, 92 | translations: statCardLocales({ name, apostrophe }), 93 | }); 94 | 95 | // Meta data for creating text nodes with createTextNode function 96 | const STATS = { 97 | stars: { 98 | icon: icons.star, 99 | label: i18n.t("statcard.totalstars"), 100 | value: totalStars, 101 | id: "stars", 102 | }, 103 | commits: { 104 | icon: icons.commits, 105 | label: `${i18n.t("statcard.commits")}${ 106 | include_all_commits ? "" : ` (${new Date().getFullYear()})` 107 | }`, 108 | value: totalCommits, 109 | id: "commits", 110 | }, 111 | prs: { 112 | icon: icons.prs, 113 | label: i18n.t("statcard.prs"), 114 | value: totalPRs, 115 | id: "prs", 116 | }, 117 | issues: { 118 | icon: icons.issues, 119 | label: i18n.t("statcard.issues"), 120 | value: totalIssues, 121 | id: "issues", 122 | }, 123 | contribs: { 124 | icon: icons.contribs, 125 | label: i18n.t("statcard.contribs"), 126 | value: contributedTo, 127 | id: "contribs", 128 | }, 129 | }; 130 | 131 | const longLocales = ["cn", "es", "fr", "pt-br", "ru", "uk-ua", "id", "my", "pl"]; 132 | const isLongLocale = longLocales.includes(locale) === true; 133 | 134 | // filter out hidden stats defined by user & create the text nodes 135 | const statItems = Object.keys(STATS) 136 | .filter((key) => !hide.includes(key)) 137 | .map((key, index) => 138 | // create the text nodes, and pass index so that we can calculate the line spacing 139 | createTextNode({ 140 | ...STATS[key], 141 | index, 142 | showIcons: show_icons, 143 | shiftValuePos: 144 | (!include_all_commits ? 50 : 20) + (isLongLocale ? 50 : 0), 145 | }), 146 | ); 147 | 148 | // Calculate the card height depending on how many items there are 149 | // but if rank circle is visible clamp the minimum height to `150` 150 | let height = Math.max( 151 | 45 + (statItems.length + 1) * lheight, 152 | hide_rank ? 0 : 150, 153 | ); 154 | 155 | // Conditionally rendered elements 156 | const rankCircle = hide_rank 157 | ? "" 158 | : ` 160 | 161 | 162 | 163 | 170 | ${rank.level} 171 | 172 | 173 | `; 174 | 175 | // the better user's score the the rank will be closer to zero so 176 | // subtracting 100 to get the progress in 100% 177 | const progress = 100 - rank.score; 178 | const cssStyles = getStyles({ 179 | titleColor, 180 | textColor, 181 | iconColor, 182 | show_icons, 183 | progress, 184 | }); 185 | 186 | const calculateTextWidth = () => { 187 | return measureText(custom_title ? custom_title : i18n.t("statcard.title")); 188 | }; 189 | 190 | const width = hide_rank 191 | ? clampValue( 192 | 50 /* padding */ + calculateTextWidth() * 2, 193 | 270 /* min */, 194 | Infinity, 195 | ) 196 | : 495; 197 | 198 | const card = new Card({ 199 | customTitle: custom_title, 200 | defaultTitle: i18n.t("statcard.title"), 201 | width, 202 | height, 203 | colors: { 204 | titleColor, 205 | textColor, 206 | iconColor, 207 | bgColor, 208 | }, 209 | }); 210 | 211 | card.setHideBorder(hide_border); 212 | card.setHideTitle(hide_title); 213 | card.setCSS(cssStyles); 214 | 215 | if (disable_animations) card.disableAnimations(); 216 | 217 | return card.render(` 218 | ${rankCircle} 219 | 220 | 221 | ${FlexLayout({ 222 | items: statItems, 223 | gap: lheight, 224 | direction: "column", 225 | }).join("")} 226 | 227 | `); 228 | }; 229 | 230 | module.exports = renderStatsCard; 231 | -------------------------------------------------------------------------------- /src/cards/top-languages-card.js: -------------------------------------------------------------------------------- 1 | const Card = require("../common/Card"); 2 | const { getCardColors, FlexLayout } = require("../common/utils"); 3 | const { createProgressNode } = require("../common/createProgressNode"); 4 | const { langCardLocales } = require("../translations"); 5 | const I18n = require("../common/I18n"); 6 | 7 | const createProgressTextNode = ({ width, color, name, progress }) => { 8 | const paddingRight = 95; 9 | const progressTextX = width - paddingRight + 10; 10 | const progressWidth = width - paddingRight; 11 | 12 | return ` 13 | ${name} 14 | ${progress}% 15 | ${createProgressNode({ 16 | x: 0, 17 | y: 25, 18 | color, 19 | width: progressWidth, 20 | progress, 21 | progressBarBackgroundColor: "#ddd", 22 | })} 23 | `; 24 | }; 25 | 26 | const createCompactLangNode = ({ lang, totalSize, x, y }) => { 27 | const percentage = ((lang.size / totalSize) * 100).toFixed(2); 28 | const color = lang.color || "#858585"; 29 | 30 | return ` 31 | 32 | 33 | 34 | ${lang.name} ${percentage}% 35 | 36 | 37 | `; 38 | }; 39 | 40 | const createLanguageTextNode = ({ langs, totalSize, x, y }) => { 41 | return langs.map((lang, index) => { 42 | if (index % 2 === 0) { 43 | return createCompactLangNode({ 44 | lang, 45 | x, 46 | y: 12.5 * index + y, 47 | totalSize, 48 | index, 49 | }); 50 | } 51 | return createCompactLangNode({ 52 | lang, 53 | x: 150, 54 | y: 12.5 + 12.5 * index, 55 | totalSize, 56 | index, 57 | }); 58 | }); 59 | }; 60 | 61 | const lowercaseTrim = (name) => name.toLowerCase().trim(); 62 | 63 | const renderTopLanguages = (topLangs, options = {}) => { 64 | const { 65 | hide_title, 66 | hide_border, 67 | card_width, 68 | title_color, 69 | text_color, 70 | bg_color, 71 | hide, 72 | theme, 73 | layout, 74 | custom_title, 75 | locale, 76 | } = options; 77 | 78 | const i18n = new I18n({ 79 | locale, 80 | translations: langCardLocales, 81 | }); 82 | 83 | let langs = Object.values(topLangs); 84 | let langsToHide = {}; 85 | 86 | // populate langsToHide map for quick lookup 87 | // while filtering out 88 | if (hide) { 89 | hide.forEach((langName) => { 90 | langsToHide[lowercaseTrim(langName)] = true; 91 | }); 92 | } 93 | 94 | // filter out langauges to be hidden 95 | langs = langs 96 | .sort((a, b) => b.size - a.size) 97 | .filter((lang) => { 98 | return !langsToHide[lowercaseTrim(lang.name)]; 99 | }); 100 | 101 | const totalLanguageSize = langs.reduce((acc, curr) => { 102 | return acc + curr.size; 103 | }, 0); 104 | 105 | // returns theme based colors with proper overrides and defaults 106 | const { titleColor, textColor, bgColor } = getCardColors({ 107 | title_color, 108 | text_color, 109 | bg_color, 110 | theme, 111 | }); 112 | 113 | let width = isNaN(card_width) ? 300 : card_width; 114 | let height = 45 + (langs.length + 1) * 40; 115 | 116 | let finalLayout = ""; 117 | 118 | // RENDER COMPACT LAYOUT 119 | if (layout === "compact") { 120 | width = width + 50; 121 | height = 90 + Math.round(langs.length / 2) * 25; 122 | 123 | // progressOffset holds the previous language's width and used to offset the next language 124 | // so that we can stack them one after another, like this: [--][----][---] 125 | let progressOffset = 0; 126 | const compactProgressBar = langs 127 | .map((lang) => { 128 | const percentage = ( 129 | (lang.size / totalLanguageSize) * 130 | (width - 50) 131 | ).toFixed(2); 132 | 133 | const progress = 134 | percentage < 10 ? parseFloat(percentage) + 10 : percentage; 135 | 136 | const output = ` 137 | 146 | `; 147 | progressOffset += parseFloat(percentage); 148 | return output; 149 | }) 150 | .join(""); 151 | 152 | finalLayout = ` 153 | 154 | 157 | 158 | ${compactProgressBar} 159 | ${createLanguageTextNode({ 160 | x: 0, 161 | y: 25, 162 | langs, 163 | totalSize: totalLanguageSize, 164 | }).join("")} 165 | `; 166 | } else { 167 | finalLayout = FlexLayout({ 168 | items: langs.map((lang) => { 169 | return createProgressTextNode({ 170 | width: width, 171 | name: lang.name, 172 | color: lang.color || "#858585", 173 | progress: ((lang.size / totalLanguageSize) * 100).toFixed(2), 174 | }); 175 | }), 176 | gap: 40, 177 | direction: "column", 178 | }).join(""); 179 | } 180 | 181 | const card = new Card({ 182 | customTitle: custom_title, 183 | defaultTitle: i18n.t("langcard.title"), 184 | width, 185 | height, 186 | colors: { 187 | titleColor, 188 | textColor, 189 | bgColor, 190 | }, 191 | }); 192 | 193 | card.disableAnimations(); 194 | card.setHideBorder(hide_border); 195 | card.setHideTitle(hide_title); 196 | card.setCSS(` 197 | .lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} } 198 | `); 199 | 200 | return card.render(` 201 | 202 | ${finalLayout} 203 | 204 | `); 205 | }; 206 | 207 | module.exports = renderTopLanguages; 208 | -------------------------------------------------------------------------------- /src/cards/wakatime-card.js: -------------------------------------------------------------------------------- 1 | const Card = require("../common/Card"); 2 | const I18n = require("../common/I18n"); 3 | const { getStyles } = require("../getStyles"); 4 | const { wakatimeCardLocales } = require("../translations"); 5 | const { getCardColors, FlexLayout } = require("../common/utils"); 6 | const { createProgressNode } = require("../common/createProgressNode"); 7 | const languageColors = require("../common/languageColors.json"); 8 | 9 | const noCodingActivityNode = ({ color, text }) => { 10 | return ` 11 | ${text} 12 | `; 13 | }; 14 | 15 | const createCompactLangNode = ({ lang, totalSize, x, y }) => { 16 | const color = languageColors[lang.name] || "#858585"; 17 | 18 | return ` 19 | 20 | 21 | 22 | ${lang.name} - ${lang.text} 23 | 24 | 25 | `; 26 | }; 27 | 28 | const createLanguageTextNode = ({ langs, totalSize, x, y }) => { 29 | return langs.map((lang, index) => { 30 | if (index % 2 === 0) { 31 | return createCompactLangNode({ 32 | lang, 33 | x: 25, 34 | y: 12.5 * index + y, 35 | totalSize, 36 | index, 37 | }); 38 | } 39 | return createCompactLangNode({ 40 | lang, 41 | x: 230, 42 | y: 12.5 + 12.5 * index, 43 | totalSize, 44 | index, 45 | }); 46 | }); 47 | }; 48 | 49 | const createTextNode = ({ 50 | id, 51 | label, 52 | value, 53 | index, 54 | percent, 55 | hideProgress, 56 | progressBarColor, 57 | progressBarBackgroundColor, 58 | }) => { 59 | const staggerDelay = (index + 3) * 150; 60 | 61 | const cardProgress = hideProgress 62 | ? null 63 | : createProgressNode({ 64 | x: 110, 65 | y: 4, 66 | progress: percent, 67 | color: progressBarColor, 68 | width: 220, 69 | name: label, 70 | progressBarBackgroundColor, 71 | }); 72 | 73 | return ` 74 | 75 | ${label}: 76 | ${value} 82 | ${cardProgress} 83 | 84 | `; 85 | }; 86 | 87 | const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { 88 | const { languages } = stats; 89 | const { 90 | hide_title = false, 91 | hide_border = false, 92 | line_height = 25, 93 | title_color, 94 | icon_color, 95 | text_color, 96 | bg_color, 97 | theme = "default", 98 | hide_progress, 99 | custom_title, 100 | locale, 101 | layout, 102 | } = options; 103 | 104 | const i18n = new I18n({ 105 | locale, 106 | translations: wakatimeCardLocales, 107 | }); 108 | 109 | const lheight = parseInt(line_height, 10); 110 | 111 | // returns theme based colors with proper overrides and defaults 112 | const { titleColor, textColor, iconColor, bgColor } = getCardColors({ 113 | title_color, 114 | icon_color, 115 | text_color, 116 | bg_color, 117 | theme, 118 | }); 119 | 120 | const statItems = languages 121 | ? languages 122 | .filter((language) => language.hours || language.minutes) 123 | .map((language) => { 124 | return createTextNode({ 125 | id: language.name, 126 | label: language.name, 127 | value: language.text, 128 | percent: language.percent, 129 | progressBarColor: titleColor, 130 | progressBarBackgroundColor: textColor, 131 | hideProgress: hide_progress, 132 | }); 133 | }) 134 | : []; 135 | 136 | // Calculate the card height depending on how many items there are 137 | // but if rank circle is visible clamp the minimum height to `150` 138 | let height = Math.max(45 + (statItems.length + 1) * lheight, 150); 139 | 140 | const cssStyles = getStyles({ 141 | titleColor, 142 | textColor, 143 | iconColor, 144 | }); 145 | 146 | let finalLayout = ""; 147 | 148 | let width = 440; 149 | 150 | // RENDER COMPACT LAYOUT 151 | if (layout === "compact") { 152 | width = width + 50; 153 | height = 90 + Math.round(languages.length / 2) * 25; 154 | 155 | // progressOffset holds the previous language's width and used to offset the next language 156 | // so that we can stack them one after another, like this: [--][----][---] 157 | let progressOffset = 0; 158 | const compactProgressBar = languages 159 | .map((lang) => { 160 | // const progress = (width * lang.percent) / 100; 161 | const progress = ((width - 50) * lang.percent) / 100; 162 | 163 | const languageColor = languageColors[lang.name] || "#858585"; 164 | 165 | const output = ` 166 | 175 | `; 176 | progressOffset += progress; 177 | return output; 178 | }) 179 | .join(""); 180 | 181 | finalLayout = ` 182 | 183 | 184 | 185 | ${compactProgressBar} 186 | ${createLanguageTextNode({ 187 | x: 0, 188 | y: 25, 189 | langs: languages, 190 | totalSize: 100, 191 | }).join("")} 192 | `; 193 | } else { 194 | finalLayout = FlexLayout({ 195 | items: statItems.length 196 | ? statItems 197 | : [ 198 | noCodingActivityNode({ 199 | color: textColor, 200 | text: i18n.t("wakatimecard.nocodingactivity"), 201 | }), 202 | ], 203 | gap: lheight, 204 | direction: "column", 205 | }).join(""); 206 | } 207 | 208 | const card = new Card({ 209 | customTitle: custom_title, 210 | defaultTitle: i18n.t("wakatimecard.title"), 211 | width: 495, 212 | height, 213 | colors: { 214 | titleColor, 215 | textColor, 216 | iconColor, 217 | bgColor, 218 | }, 219 | }); 220 | 221 | card.setHideBorder(hide_border); 222 | card.setHideTitle(hide_title); 223 | card.setCSS( 224 | ` 225 | ${cssStyles} 226 | .lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} } 227 | `, 228 | ); 229 | 230 | return card.render(` 231 | 232 | ${finalLayout} 233 | 234 | `); 235 | }; 236 | 237 | module.exports = renderWakatimeCard; 238 | exports.createProgressNode = createProgressNode; 239 | -------------------------------------------------------------------------------- /src/common/Card.js: -------------------------------------------------------------------------------- 1 | const { FlexLayout, encodeHTML } = require("../common/utils"); 2 | const { getAnimations } = require("../getStyles"); 3 | 4 | class Card { 5 | constructor({ 6 | width = 100, 7 | height = 100, 8 | colors = {}, 9 | customTitle, 10 | defaultTitle = "", 11 | titlePrefixIcon, 12 | }) { 13 | this.width = width; 14 | this.height = height; 15 | 16 | this.hideBorder = false; 17 | this.hideTitle = false; 18 | 19 | // returns theme based colors with proper overrides and defaults 20 | this.colors = colors; 21 | this.title = 22 | customTitle !== undefined 23 | ? encodeHTML(customTitle) 24 | : encodeHTML(defaultTitle); 25 | 26 | this.css = ""; 27 | 28 | this.paddingX = 25; 29 | this.paddingY = 35; 30 | this.titlePrefixIcon = titlePrefixIcon; 31 | this.animations = true; 32 | } 33 | 34 | disableAnimations() { 35 | this.animations = false; 36 | } 37 | 38 | setCSS(value) { 39 | this.css = value; 40 | } 41 | 42 | setHideBorder(value) { 43 | this.hideBorder = value; 44 | } 45 | 46 | setHideTitle(value) { 47 | this.hideTitle = value; 48 | if (value) { 49 | this.height -= 30; 50 | } 51 | } 52 | 53 | setTitle(text) { 54 | this.title = text; 55 | } 56 | 57 | renderTitle() { 58 | const titleText = ` 59 | ${this.title} 65 | `; 66 | 67 | const prefixIcon = ` 68 | 77 | ${this.titlePrefixIcon} 78 | 79 | `; 80 | return ` 81 | 85 | ${FlexLayout({ 86 | items: [this.titlePrefixIcon && prefixIcon, titleText], 87 | gap: 25, 88 | }).join("")} 89 | 90 | `; 91 | } 92 | 93 | renderGradient() { 94 | if (typeof this.colors.bgColor !== "object") return; 95 | 96 | const gradients = this.colors.bgColor.slice(1); 97 | return typeof this.colors.bgColor === "object" 98 | ? ` 99 | 100 | 104 | ${gradients.map((grad, index) => { 105 | let offset = (index * 100) / (gradients.length - 1); 106 | return ``; 107 | })} 108 | 109 | 110 | ` 111 | : ""; 112 | } 113 | 114 | render(body) { 115 | return ` 116 | 123 | 138 | 139 | ${this.renderGradient()} 140 | 141 | 156 | 157 | ${this.hideTitle ? "" : this.renderTitle()} 158 | 159 | 165 | ${body} 166 | 167 | 168 | `; 169 | } 170 | } 171 | 172 | module.exports = Card; 173 | -------------------------------------------------------------------------------- /src/common/I18n.js: -------------------------------------------------------------------------------- 1 | class I18n { 2 | constructor({ locale, translations }) { 3 | this.locale = locale; 4 | this.translations = translations; 5 | this.fallbackLocale = "en"; 6 | } 7 | 8 | t(str) { 9 | if (!this.translations[str]) { 10 | throw new Error(`${str} Translation string not found`); 11 | } 12 | 13 | if (!this.translations[str][this.locale || this.fallbackLocale]) { 14 | throw new Error(`${str} Translation locale not found`); 15 | } 16 | 17 | return this.translations[str][this.locale || this.fallbackLocale]; 18 | } 19 | } 20 | 21 | module.exports = I18n; 22 | -------------------------------------------------------------------------------- /src/common/blacklist.js: -------------------------------------------------------------------------------- 1 | const blacklist = ["renovate-bot", "technote-space", "sw-yx"]; 2 | 3 | module.exports = blacklist; 4 | -------------------------------------------------------------------------------- /src/common/createProgressNode.js: -------------------------------------------------------------------------------- 1 | const { clampValue } = require("../common/utils"); 2 | 3 | const createProgressNode = ({ 4 | x, 5 | y, 6 | width, 7 | color, 8 | progress, 9 | progressBarBackgroundColor, 10 | }) => { 11 | const progressPercentage = clampValue(progress, 2, 100); 12 | 13 | return ` 14 | 15 | 16 | 23 | 24 | 25 | `; 26 | }; 27 | 28 | exports.createProgressNode = createProgressNode; 29 | -------------------------------------------------------------------------------- /src/common/icons.js: -------------------------------------------------------------------------------- 1 | const icons = { 2 | star: ``, 3 | commits: ``, 4 | prs: ``, 5 | issues: ``, 6 | icon: ``, 7 | contribs: ``, 8 | fork: ``, 9 | }; 10 | 11 | module.exports = icons; 12 | -------------------------------------------------------------------------------- /src/common/languageColors.json: -------------------------------------------------------------------------------- 1 | { 2 | "1C Enterprise": "#814CCC", 3 | "4D": null, 4 | "ABAP": "#E8274B", 5 | "ActionScript": "#882B0F", 6 | "Ada": "#02f88c", 7 | "Agda": "#315665", 8 | "AGS Script": "#B9D9FF", 9 | "AL": "#3AA2B5", 10 | "Alloy": "#64C800", 11 | "Alpine Abuild": null, 12 | "AMPL": "#E6EFBB", 13 | "AngelScript": "#C7D7DC", 14 | "ANTLR": "#9DC3FF", 15 | "Apex": "#1797c0", 16 | "API Blueprint": "#2ACCA8", 17 | "APL": "#5A8164", 18 | "Apollo Guidance Computer": "#0B3D91", 19 | "AppleScript": "#101F1F", 20 | "Arc": "#aa2afe", 21 | "ASL": null, 22 | "ASP.NET": "#9400ff", 23 | "AspectJ": "#a957b0", 24 | "Assembly": "#6E4C13", 25 | "Asymptote": "#ff0000", 26 | "ATS": "#1ac620", 27 | "Augeas": null, 28 | "AutoHotkey": "#6594b9", 29 | "AutoIt": "#1C3552", 30 | "Awk": null, 31 | "Ballerina": "#FF5000", 32 | "Batchfile": "#C1F12E", 33 | "Befunge": null, 34 | "Bison": "#6A463F", 35 | "BitBake": null, 36 | "Blade": "#f7523f", 37 | "BlitzBasic": null, 38 | "BlitzMax": "#cd6400", 39 | "Bluespec": null, 40 | "Boo": "#d4bec1", 41 | "Brainfuck": "#2F2530", 42 | "Brightscript": null, 43 | "C": "#555555", 44 | "C#": "#178600", 45 | "C++": "#f34b7d", 46 | "C2hs Haskell": null, 47 | "Cap'n Proto": null, 48 | "CartoCSS": null, 49 | "Ceylon": "#dfa535", 50 | "Chapel": "#8dc63f", 51 | "Charity": null, 52 | "ChucK": null, 53 | "Cirru": "#ccccff", 54 | "Clarion": "#db901e", 55 | "Classic ASP": "#6a40fd", 56 | "Clean": "#3F85AF", 57 | "Click": "#E4E6F3", 58 | "CLIPS": null, 59 | "Clojure": "#db5855", 60 | "CMake": null, 61 | "COBOL": null, 62 | "CodeQL": null, 63 | "CoffeeScript": "#244776", 64 | "ColdFusion": "#ed2cd6", 65 | "ColdFusion CFC": "#ed2cd6", 66 | "Common Lisp": "#3fb68b", 67 | "Common Workflow Language": "#B5314C", 68 | "Component Pascal": "#B0CE4E", 69 | "Cool": null, 70 | "Coq": null, 71 | "Crystal": "#000100", 72 | "CSON": "#244776", 73 | "Csound": null, 74 | "Csound Document": null, 75 | "Csound Score": null, 76 | "CSS": "#563d7c", 77 | "Cuda": "#3A4E3A", 78 | "CWeb": null, 79 | "Cycript": null, 80 | "Cython": null, 81 | "D": "#ba595e", 82 | "Dafny": "#FFEC25", 83 | "Dart": "#00B4AB", 84 | "DataWeave": "#003a52", 85 | "Dhall": "#dfafff", 86 | "DIGITAL Command Language": null, 87 | "DM": "#447265", 88 | "Dockerfile": "#384d54", 89 | "Dogescript": "#cca760", 90 | "DTrace": null, 91 | "Dylan": "#6c616e", 92 | "E": "#ccce35", 93 | "eC": "#913960", 94 | "ECL": "#8a1267", 95 | "ECLiPSe": null, 96 | "Eiffel": "#4d6977", 97 | "EJS": "#a91e50", 98 | "Elixir": "#6e4a7e", 99 | "Elm": "#60B5CC", 100 | "Emacs Lisp": "#c065db", 101 | "EmberScript": "#FFF4F3", 102 | "EQ": "#a78649", 103 | "Erlang": "#B83998", 104 | "F#": "#b845fc", 105 | "F*": "#572e30", 106 | "Factor": "#636746", 107 | "Fancy": "#7b9db4", 108 | "Fantom": "#14253c", 109 | "Faust": "#c37240", 110 | "Filebench WML": null, 111 | "Filterscript": null, 112 | "fish": null, 113 | "FLUX": "#88ccff", 114 | "Forth": "#341708", 115 | "Fortran": "#4d41b1", 116 | "Fortran Free Form": null, 117 | "FreeMarker": "#0050b2", 118 | "Frege": "#00cafe", 119 | "Futhark": "#5f021f", 120 | "G-code": "#D08CF2", 121 | "Game Maker Language": "#71b417", 122 | "GAML": "#FFC766", 123 | "GAMS": null, 124 | "GAP": null, 125 | "GCC Machine Description": null, 126 | "GDB": null, 127 | "GDScript": "#355570", 128 | "Genie": "#fb855d", 129 | "Genshi": null, 130 | "Gentoo Ebuild": null, 131 | "Gentoo Eclass": null, 132 | "Gherkin": "#5B2063", 133 | "GLSL": null, 134 | "Glyph": "#c1ac7f", 135 | "Gnuplot": "#f0a9f0", 136 | "Go": "#00ADD8", 137 | "Golo": "#88562A", 138 | "Gosu": "#82937f", 139 | "Grace": null, 140 | "Grammatical Framework": "#ff0000", 141 | "GraphQL": "#e10098", 142 | "Groovy": "#e69f56", 143 | "Groovy Server Pages": null, 144 | "Hack": "#878787", 145 | "Haml": "#ece2a9", 146 | "Handlebars": "#f7931e", 147 | "Harbour": "#0e60e3", 148 | "Haskell": "#5e5086", 149 | "Haxe": "#df7900", 150 | "HCL": null, 151 | "HiveQL": "#dce200", 152 | "HLSL": null, 153 | "HolyC": "#ffefaf", 154 | "HTML": "#e34c26", 155 | "Hy": "#7790B2", 156 | "HyPhy": null, 157 | "IDL": "#a3522f", 158 | "Idris": "#b30000", 159 | "IGOR Pro": "#0000cc", 160 | "Inform 7": null, 161 | "Inno Setup": null, 162 | "Io": "#a9188d", 163 | "Ioke": "#078193", 164 | "Isabelle": "#FEFE00", 165 | "Isabelle ROOT": null, 166 | "J": "#9EEDFF", 167 | "Jasmin": null, 168 | "Java": "#b07219", 169 | "Java Server Pages": null, 170 | "JavaScript": "#f1e05a", 171 | "JavaScript+ERB": null, 172 | "JFlex": "#DBCA00", 173 | "Jison": null, 174 | "Jison Lex": null, 175 | "Jolie": "#843179", 176 | "JSONiq": "#40d47e", 177 | "Jsonnet": "#0064bd", 178 | "JSX": null, 179 | "Julia": "#a270ba", 180 | "Jupyter Notebook": "#DA5B0B", 181 | "Kaitai Struct": "#773b37", 182 | "Kotlin": "#F18E33", 183 | "KRL": "#28430A", 184 | "LabVIEW": null, 185 | "Lasso": "#999999", 186 | "Latte": "#f2a542", 187 | "Lean": null, 188 | "Less": "#1d365d", 189 | "Lex": "#DBCA00", 190 | "LFE": "#4C3023", 191 | "LilyPond": null, 192 | "Limbo": null, 193 | "Literate Agda": null, 194 | "Literate CoffeeScript": null, 195 | "Literate Haskell": null, 196 | "LiveScript": "#499886", 197 | "LLVM": "#185619", 198 | "Logos": null, 199 | "Logtalk": null, 200 | "LOLCODE": "#cc9900", 201 | "LookML": "#652B81", 202 | "LoomScript": null, 203 | "LSL": "#3d9970", 204 | "Lua": "#000080", 205 | "M": null, 206 | "M4": null, 207 | "M4Sugar": null, 208 | "Macaulay2": "#d8ffff", 209 | "Makefile": "#427819", 210 | "Mako": null, 211 | "Markdown": "#083fa1", 212 | "Marko": "#42bff2", 213 | "Mask": "#f97732", 214 | "Mathematica": null, 215 | "MATLAB": "#e16737", 216 | "Max": "#c4a79c", 217 | "MAXScript": "#00a6a6", 218 | "mcfunction": "#E22837", 219 | "Mercury": "#ff2b2b", 220 | "Meson": "#007800", 221 | "Metal": "#8f14e9", 222 | "MiniD": null, 223 | "Mirah": "#c7a938", 224 | "mIRC Script": "#3d57c3", 225 | "MLIR": "#5EC8DB", 226 | "Modelica": null, 227 | "Modula-2": null, 228 | "Modula-3": "#223388", 229 | "Module Management System": null, 230 | "Monkey": null, 231 | "Moocode": null, 232 | "MoonScript": null, 233 | "Motorola 68K Assembly": null, 234 | "MQL4": "#62A8D6", 235 | "MQL5": "#4A76B8", 236 | "MTML": "#b7e1f4", 237 | "MUF": null, 238 | "mupad": null, 239 | "Myghty": null, 240 | "NASL": null, 241 | "NCL": "#28431f", 242 | "Nearley": "#990000", 243 | "Nemerle": "#3d3c6e", 244 | "nesC": "#94B0C7", 245 | "NetLinx": "#0aa0ff", 246 | "NetLinx+ERB": "#747faa", 247 | "NetLogo": "#ff6375", 248 | "NewLisp": "#87AED7", 249 | "Nextflow": "#3ac486", 250 | "Nim": "#ffc200", 251 | "Nit": "#009917", 252 | "Nix": "#7e7eff", 253 | "NSIS": null, 254 | "Nu": "#c9df40", 255 | "NumPy": "#9C8AF9", 256 | "Objective-C": "#438eff", 257 | "Objective-C++": "#6866fb", 258 | "Objective-J": "#ff0c5a", 259 | "ObjectScript": "#424893", 260 | "OCaml": "#3be133", 261 | "Odin": "#60AFFE", 262 | "Omgrofl": "#cabbff", 263 | "ooc": "#b0b77e", 264 | "Opa": null, 265 | "Opal": "#f7ede0", 266 | "Open Policy Agent": null, 267 | "OpenCL": null, 268 | "OpenEdge ABL": null, 269 | "OpenQASM": "#AA70FF", 270 | "OpenRC runscript": null, 271 | "OpenSCAD": null, 272 | "Ox": null, 273 | "Oxygene": "#cdd0e3", 274 | "Oz": "#fab738", 275 | "P4": "#7055b5", 276 | "Pan": "#cc0000", 277 | "Papyrus": "#6600cc", 278 | "Parrot": "#f3ca0a", 279 | "Parrot Assembly": null, 280 | "Parrot Internal Representation": null, 281 | "Pascal": "#E3F171", 282 | "Pawn": "#dbb284", 283 | "Pep8": "#C76F5B", 284 | "Perl": "#0298c3", 285 | "PHP": "#4F5D95", 286 | "PicoLisp": null, 287 | "PigLatin": "#fcd7de", 288 | "Pike": "#005390", 289 | "PLpgSQL": null, 290 | "PLSQL": "#dad8d8", 291 | "PogoScript": "#d80074", 292 | "Pony": null, 293 | "PostScript": "#da291c", 294 | "POV-Ray SDL": null, 295 | "PowerBuilder": "#8f0f8d", 296 | "PowerShell": "#012456", 297 | "Prisma": "#0c344b", 298 | "Processing": "#0096D8", 299 | "Prolog": "#74283c", 300 | "Propeller Spin": "#7fa2a7", 301 | "Pug": "#a86454", 302 | "Puppet": "#302B6D", 303 | "PureBasic": "#5a6986", 304 | "PureScript": "#1D222D", 305 | "Python": "#3572A5", 306 | "Python console": null, 307 | "q": "#0040cd", 308 | "Q#": "#fed659", 309 | "QMake": null, 310 | "QML": "#44a51c", 311 | "Qt Script": "#00b841", 312 | "Quake": "#882233", 313 | "R": "#198CE7", 314 | "Racket": "#3c5caa", 315 | "Ragel": "#9d5200", 316 | "Raku": "#0000fb", 317 | "RAML": "#77d9fb", 318 | "Rascal": "#fffaa0", 319 | "REALbasic": null, 320 | "Reason": "#ff5847", 321 | "Rebol": "#358a5b", 322 | "Red": "#f50000", 323 | "Redcode": null, 324 | "Ren'Py": "#ff7f7f", 325 | "RenderScript": null, 326 | "REXX": null, 327 | "Ring": "#2D54CB", 328 | "Riot": "#A71E49", 329 | "RobotFramework": null, 330 | "Roff": "#ecdebe", 331 | "Rouge": "#cc0088", 332 | "RPC": null, 333 | "Ruby": "#701516", 334 | "RUNOFF": "#665a4e", 335 | "Rust": "#dea584", 336 | "Sage": null, 337 | "SaltStack": "#646464", 338 | "SAS": "#B34936", 339 | "Sass": "#a53b70", 340 | "Scala": "#c22d40", 341 | "Scheme": "#1e4aec", 342 | "Scilab": null, 343 | "SCSS": "#c6538c", 344 | "sed": "#64b970", 345 | "Self": "#0579aa", 346 | "ShaderLab": null, 347 | "Shell": "#89e051", 348 | "ShellSession": null, 349 | "Shen": "#120F14", 350 | "Sieve": null, 351 | "Slash": "#007eff", 352 | "Slice": "#003fa2", 353 | "Slim": "#2b2b2b", 354 | "Smali": null, 355 | "Smalltalk": "#596706", 356 | "Smarty": null, 357 | "SmPL": "#c94949", 358 | "SMT": null, 359 | "Solidity": "#AA6746", 360 | "SourcePawn": "#f69e1d", 361 | "SQF": "#3F3F3F", 362 | "SQLPL": null, 363 | "Squirrel": "#800000", 364 | "SRecode Template": "#348a34", 365 | "Stan": "#b2011d", 366 | "Standard ML": "#dc566d", 367 | "Starlark": "#76d275", 368 | "Stata": null, 369 | "Stylus": "#ff6347", 370 | "SuperCollider": "#46390b", 371 | "Svelte": "#ff3e00", 372 | "SVG": "#ff9900", 373 | "Swift": "#ffac45", 374 | "SWIG": null, 375 | "SystemVerilog": "#DAE1C2", 376 | "Tcl": "#e4cc98", 377 | "Tcsh": null, 378 | "Terra": "#00004c", 379 | "TeX": "#3D6117", 380 | "Thrift": null, 381 | "TI Program": "#A0AA87", 382 | "TLA": null, 383 | "TSQL": null, 384 | "TSX": null, 385 | "Turing": "#cf142b", 386 | "Twig": "#c1d026", 387 | "TXL": null, 388 | "TypeScript": "#2b7489", 389 | "Unified Parallel C": "#4e3617", 390 | "Unix Assembly": null, 391 | "Uno": "#9933cc", 392 | "UnrealScript": "#a54c4d", 393 | "UrWeb": null, 394 | "V": "#4f87c4", 395 | "Vala": "#fbe5cd", 396 | "VBA": "#867db1", 397 | "VBScript": "#15dcdc", 398 | "VCL": "#148AA8", 399 | "Verilog": "#b2b7f8", 400 | "VHDL": "#adb2cb", 401 | "Vim script": "#199f4b", 402 | "Visual Basic .NET": "#945db7", 403 | "Volt": "#1F1F1F", 404 | "Vue": "#2c3e50", 405 | "wdl": "#42f1f4", 406 | "WebAssembly": "#04133b", 407 | "WebIDL": null, 408 | "wisp": "#7582D1", 409 | "Wollok": "#a23738", 410 | "X10": "#4B6BEF", 411 | "xBase": "#403a40", 412 | "XC": "#99DA07", 413 | "Xojo": null, 414 | "XProc": null, 415 | "XQuery": "#5232e7", 416 | "XS": null, 417 | "XSLT": "#EB8CEB", 418 | "Xtend": null, 419 | "Yacc": "#4B6C4B", 420 | "YAML": "#cb171e", 421 | "YARA": "#220000", 422 | "YASnippet": "#32AB90", 423 | "ZAP": "#0d665e", 424 | "Zeek": null, 425 | "ZenScript": "#00BCD1", 426 | "Zephir": "#118f9e", 427 | "Zig": "#ec915c", 428 | "ZIL": "#dc75e5", 429 | "Zimpl": null 430 | } -------------------------------------------------------------------------------- /src/common/retryer.js: -------------------------------------------------------------------------------- 1 | const { logger, CustomError } = require("../common/utils"); 2 | 3 | const retryer = async (fetcher, variables, retries = 0) => { 4 | if (retries > 7) { 5 | throw new CustomError("Maximum retries exceeded", CustomError.MAX_RETRY); 6 | } 7 | try { 8 | // try to fetch with the first token since RETRIES is 0 index i'm adding +1 9 | let response = await fetcher( 10 | variables, 11 | process.env[`PAT_${retries + 1}`], 12 | retries, 13 | ); 14 | 15 | // prettier-ignore 16 | const isRateExceeded = response.data.errors && response.data.errors[0].type === "RATE_LIMITED"; 17 | 18 | // if rate limit is hit increase the RETRIES and recursively call the retryer 19 | // with username, and current RETRIES 20 | if (isRateExceeded) { 21 | logger.log(`PAT_${retries + 1} Failed`); 22 | retries++; 23 | // directly return from the function 24 | return retryer(fetcher, variables, retries); 25 | } 26 | 27 | // finally return the response 28 | return response; 29 | } catch (err) { 30 | // prettier-ignore 31 | // also checking for bad credentials if any tokens gets invalidated 32 | const isBadCredential = err.response.data && err.response.data.message === "Bad credentials"; 33 | 34 | if (isBadCredential) { 35 | logger.log(`PAT_${retries + 1} Failed`); 36 | retries++; 37 | // directly return from the function 38 | return retryer(fetcher, variables, retries); 39 | } 40 | } 41 | }; 42 | 43 | module.exports = retryer; 44 | -------------------------------------------------------------------------------- /src/common/utils.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const wrap = require("word-wrap"); 3 | const themes = require("../../themes"); 4 | 5 | const renderError = (message, secondaryMessage = "") => { 6 | return ` 7 | 8 | 13 | 14 | Something went wrong! file an issue at https://git.io/JJmN9 15 | 16 | ${encodeHTML(message)} 17 | ${secondaryMessage} 18 | 19 | 20 | `; 21 | }; 22 | 23 | // https://stackoverflow.com/a/48073476/10629172 24 | function encodeHTML(str) { 25 | return str 26 | .replace(/[\u00A0-\u9999<>&](?!#)/gim, (i) => { 27 | return "&#" + i.charCodeAt(0) + ";"; 28 | }) 29 | .replace(/\u0008/gim, ""); 30 | } 31 | 32 | function kFormatter(num) { 33 | return Math.abs(num) > 999 34 | ? Math.sign(num) * (Math.abs(num) / 1000).toFixed(1) + "k" 35 | : Math.sign(num) * Math.abs(num); 36 | } 37 | 38 | function isValidHexColor(hexColor) { 39 | return new RegExp( 40 | /^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4})$/, 41 | ).test(hexColor); 42 | } 43 | 44 | function parseBoolean(value) { 45 | if (value === "true") { 46 | return true; 47 | } else if (value === "false") { 48 | return false; 49 | } else { 50 | return value; 51 | } 52 | } 53 | 54 | function parseArray(str) { 55 | if (!str) return []; 56 | return str.split(","); 57 | } 58 | 59 | function clampValue(number, min, max) { 60 | return Math.max(min, Math.min(number, max)); 61 | } 62 | 63 | function isValidGradient(colors) { 64 | return isValidHexColor(colors[1]) && isValidHexColor(colors[2]); 65 | } 66 | 67 | function fallbackColor(color, fallbackColor) { 68 | let colors = color.split(","); 69 | let gradient = null; 70 | 71 | if (colors.length > 1 && isValidGradient(colors)) { 72 | gradient = colors; 73 | } 74 | 75 | return ( 76 | (gradient ? gradient : isValidHexColor(color) && `#${color}`) || 77 | fallbackColor 78 | ); 79 | } 80 | 81 | function request(data, headers) { 82 | return axios({ 83 | url: "https://api.github.com/graphql", 84 | method: "post", 85 | headers, 86 | data, 87 | }); 88 | } 89 | 90 | /** 91 | * 92 | * @param {String[]} items 93 | * @param {Number} gap 94 | * @param {string} direction 95 | * 96 | * @description 97 | * Auto layout utility, allows us to layout things 98 | * vertically or horizontally with proper gaping 99 | */ 100 | function FlexLayout({ items, gap, direction }) { 101 | // filter() for filtering out empty strings 102 | return items.filter(Boolean).map((item, i) => { 103 | let transform = `translate(${gap * i}, 0)`; 104 | if (direction === "column") { 105 | transform = `translate(0, ${gap * i})`; 106 | } 107 | return `${item}`; 108 | }); 109 | } 110 | 111 | // returns theme based colors with proper overrides and defaults 112 | function getCardColors({ 113 | title_color, 114 | text_color, 115 | icon_color, 116 | bg_color, 117 | theme, 118 | fallbackTheme = "default", 119 | }) { 120 | const defaultTheme = themes[fallbackTheme]; 121 | const selectedTheme = themes[theme] || defaultTheme; 122 | 123 | // get the color provided by the user else the theme color 124 | // finally if both colors are invalid fallback to default theme 125 | const titleColor = fallbackColor( 126 | title_color || selectedTheme.title_color, 127 | "#" + defaultTheme.title_color, 128 | ); 129 | const iconColor = fallbackColor( 130 | icon_color || selectedTheme.icon_color, 131 | "#" + defaultTheme.icon_color, 132 | ); 133 | const textColor = fallbackColor( 134 | text_color || selectedTheme.text_color, 135 | "#" + defaultTheme.text_color, 136 | ); 137 | const bgColor = fallbackColor( 138 | bg_color || selectedTheme.bg_color, 139 | "#" + defaultTheme.bg_color, 140 | ); 141 | 142 | return { titleColor, iconColor, textColor, bgColor }; 143 | } 144 | 145 | function wrapTextMultiline(text, width = 60, maxLines = 3) { 146 | const wrapped = wrap(encodeHTML(text), { width }) 147 | .split("\n") // Split wrapped lines to get an array of lines 148 | .map((line) => line.trim()); // Remove leading and trailing whitespace of each line 149 | 150 | const lines = wrapped.slice(0, maxLines); // Only consider maxLines lines 151 | 152 | // Add "..." to the last line if the text exceeds maxLines 153 | if (wrapped.length > maxLines) { 154 | lines[maxLines - 1] += "..."; 155 | } 156 | 157 | // Remove empty lines if text fits in less than maxLines lines 158 | const multiLineText = lines.filter(Boolean); 159 | return multiLineText; 160 | } 161 | 162 | const noop = () => {}; 163 | // return console instance based on the environment 164 | const logger = 165 | process.env.NODE_ENV !== "test" ? console : { log: noop, error: noop }; 166 | 167 | const CONSTANTS = { 168 | THIRTY_MINUTES: 1800, 169 | TWO_HOURS: 7200, 170 | FOUR_HOURS: 14400, 171 | ONE_DAY: 86400, 172 | }; 173 | 174 | const SECONDARY_ERROR_MESSAGES = { 175 | MAX_RETRY: 176 | "Please add an env variable called PAT_1 with your github token in vercel", 177 | USER_NOT_FOUND: "Make sure the provided username is not an organization", 178 | }; 179 | 180 | class CustomError extends Error { 181 | constructor(message, type) { 182 | super(message); 183 | this.type = type; 184 | this.secondaryMessage = SECONDARY_ERROR_MESSAGES[type] || "adsad"; 185 | } 186 | 187 | static MAX_RETRY = "MAX_RETRY"; 188 | static USER_NOT_FOUND = "USER_NOT_FOUND"; 189 | } 190 | 191 | // https://stackoverflow.com/a/48172630/10629172 192 | function measureText(str, fontSize = 10) { 193 | // prettier-ignore 194 | const widths = [ 195 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 196 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 197 | 0, 0, 0, 0, 0.2796875, 0.2765625, 198 | 0.3546875, 0.5546875, 0.5546875, 0.8890625, 0.665625, 0.190625, 199 | 0.3328125, 0.3328125, 0.3890625, 0.5828125, 0.2765625, 0.3328125, 200 | 0.2765625, 0.3015625, 0.5546875, 0.5546875, 0.5546875, 0.5546875, 201 | 0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.5546875, 202 | 0.2765625, 0.2765625, 0.584375, 0.5828125, 0.584375, 0.5546875, 203 | 1.0140625, 0.665625, 0.665625, 0.721875, 0.721875, 0.665625, 204 | 0.609375, 0.7765625, 0.721875, 0.2765625, 0.5, 0.665625, 205 | 0.5546875, 0.8328125, 0.721875, 0.7765625, 0.665625, 0.7765625, 206 | 0.721875, 0.665625, 0.609375, 0.721875, 0.665625, 0.94375, 207 | 0.665625, 0.665625, 0.609375, 0.2765625, 0.3546875, 0.2765625, 208 | 0.4765625, 0.5546875, 0.3328125, 0.5546875, 0.5546875, 0.5, 209 | 0.5546875, 0.5546875, 0.2765625, 0.5546875, 0.5546875, 0.221875, 210 | 0.240625, 0.5, 0.221875, 0.8328125, 0.5546875, 0.5546875, 211 | 0.5546875, 0.5546875, 0.3328125, 0.5, 0.2765625, 0.5546875, 212 | 0.5, 0.721875, 0.5, 0.5, 0.5, 0.3546875, 0.259375, 0.353125, 0.5890625, 213 | ]; 214 | 215 | const avg = 0.5279276315789471; 216 | return ( 217 | str 218 | .split("") 219 | .map((c) => 220 | c.charCodeAt(0) < widths.length ? widths[c.charCodeAt(0)] : avg, 221 | ) 222 | .reduce((cur, acc) => acc + cur) * fontSize 223 | ); 224 | } 225 | 226 | module.exports = { 227 | renderError, 228 | kFormatter, 229 | encodeHTML, 230 | isValidHexColor, 231 | request, 232 | parseArray, 233 | parseBoolean, 234 | fallbackColor, 235 | FlexLayout, 236 | getCardColors, 237 | clampValue, 238 | wrapTextMultiline, 239 | measureText, 240 | logger, 241 | CONSTANTS, 242 | CustomError, 243 | }; 244 | -------------------------------------------------------------------------------- /src/fetchers/repo-fetcher.js: -------------------------------------------------------------------------------- 1 | const { request } = require("../common/utils"); 2 | const retryer = require("../common/retryer"); 3 | 4 | const fetcher = (variables, token) => { 5 | return request( 6 | { 7 | query: ` 8 | fragment RepoInfo on Repository { 9 | name 10 | nameWithOwner 11 | isPrivate 12 | isArchived 13 | isTemplate 14 | stargazers { 15 | totalCount 16 | } 17 | description 18 | primaryLanguage { 19 | color 20 | id 21 | name 22 | } 23 | forkCount 24 | } 25 | query getRepo($login: String!, $repo: String!) { 26 | user(login: $login) { 27 | repository(name: $repo) { 28 | ...RepoInfo 29 | } 30 | } 31 | organization(login: $login) { 32 | repository(name: $repo) { 33 | ...RepoInfo 34 | } 35 | } 36 | } 37 | `, 38 | variables, 39 | }, 40 | { 41 | Authorization: `bearer ${token}`, 42 | }, 43 | ); 44 | }; 45 | 46 | async function fetchRepo(username, reponame) { 47 | if (!username || !reponame) { 48 | throw new Error("Invalid username or reponame"); 49 | } 50 | 51 | let res = await retryer(fetcher, { login: username, repo: reponame }); 52 | 53 | const data = res.data.data; 54 | 55 | if (!data.user && !data.organization) { 56 | throw new Error("Not found"); 57 | } 58 | 59 | const isUser = data.organization === null && data.user; 60 | const isOrg = data.user === null && data.organization; 61 | 62 | if (isUser) { 63 | if (!data.user.repository || data.user.repository.isPrivate) { 64 | throw new Error("User Repository Not found"); 65 | } 66 | return data.user.repository; 67 | } 68 | 69 | if (isOrg) { 70 | if ( 71 | !data.organization.repository || 72 | data.organization.repository.isPrivate 73 | ) { 74 | throw new Error("Organization Repository Not found"); 75 | } 76 | return data.organization.repository; 77 | } 78 | } 79 | 80 | module.exports = fetchRepo; 81 | -------------------------------------------------------------------------------- /src/fetchers/stats-fetcher.js: -------------------------------------------------------------------------------- 1 | const { request, logger, CustomError } = require("../common/utils"); 2 | const axios = require("axios"); 3 | const retryer = require("../common/retryer"); 4 | const calculateRank = require("../calculateRank"); 5 | const githubUsernameRegex = require("github-username-regex"); 6 | 7 | require("dotenv").config(); 8 | 9 | const fetcher = (variables, token) => { 10 | return request( 11 | { 12 | query: ` 13 | query userInfo($login: String!) { 14 | user(login: $login) { 15 | name 16 | login 17 | contributionsCollection { 18 | totalCommitContributions 19 | restrictedContributionsCount 20 | } 21 | repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) { 22 | totalCount 23 | } 24 | pullRequests(first: 1) { 25 | totalCount 26 | } 27 | issues(first: 1) { 28 | totalCount 29 | } 30 | followers { 31 | totalCount 32 | } 33 | repositories(first: 100, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}) { 34 | totalCount 35 | nodes { 36 | stargazers { 37 | totalCount 38 | } 39 | } 40 | } 41 | } 42 | } 43 | `, 44 | variables, 45 | }, 46 | { 47 | Authorization: `bearer ${token}`, 48 | }, 49 | ); 50 | }; 51 | 52 | // https://github.com/SrGobi/github-readme-stats/issues/92#issuecomment-661026467 53 | // https://github.com/SrGobi/github-readme-stats/pull/211/ 54 | const totalCommitsFetcher = async (username) => { 55 | if (!githubUsernameRegex.test(username)) { 56 | logger.log("Invalid username"); 57 | return 0; 58 | } 59 | 60 | // https://developer.github.com/v3/search/#search-commits 61 | const fetchTotalCommits = (variables, token) => { 62 | return axios({ 63 | method: "get", 64 | url: `https://api.github.com/search/commits?q=author:${variables.login}`, 65 | headers: { 66 | "Content-Type": "application/json", 67 | Accept: "application/vnd.github.cloak-preview", 68 | Authorization: `bearer ${token}`, 69 | }, 70 | }); 71 | }; 72 | 73 | try { 74 | let res = await retryer(fetchTotalCommits, { login: username }); 75 | if (res.data.total_count) { 76 | return res.data.total_count; 77 | } 78 | } catch (err) { 79 | logger.log(err); 80 | // just return 0 if there is something wrong so that 81 | // we don't break the whole app 82 | return 0; 83 | } 84 | }; 85 | 86 | async function fetchStats( 87 | username, 88 | count_private = false, 89 | include_all_commits = false, 90 | ) { 91 | if (!username) throw Error("Invalid username"); 92 | 93 | const stats = { 94 | name: "", 95 | totalPRs: 0, 96 | totalCommits: 0, 97 | totalIssues: 0, 98 | totalStars: 0, 99 | contributedTo: 0, 100 | rank: { level: "C", score: 0 }, 101 | }; 102 | 103 | let res = await retryer(fetcher, { login: username }); 104 | 105 | if (res.data.errors) { 106 | logger.error(res.data.errors); 107 | throw new CustomError( 108 | res.data.errors[0].message || "Could not fetch user", 109 | CustomError.USER_NOT_FOUND, 110 | ); 111 | } 112 | 113 | const user = res.data.data.user; 114 | 115 | stats.name = user.name || user.login; 116 | stats.totalIssues = user.issues.totalCount; 117 | 118 | // normal commits 119 | stats.totalCommits = user.contributionsCollection.totalCommitContributions; 120 | 121 | // if include_all_commits then just get that, 122 | // since totalCommitsFetcher already sends totalCommits no need to += 123 | if (include_all_commits) { 124 | stats.totalCommits = await totalCommitsFetcher(username); 125 | } 126 | 127 | // if count_private then add private commits to totalCommits so far. 128 | if (count_private) { 129 | stats.totalCommits += 130 | user.contributionsCollection.restrictedContributionsCount; 131 | } 132 | 133 | stats.totalPRs = user.pullRequests.totalCount; 134 | stats.contributedTo = user.repositoriesContributedTo.totalCount; 135 | 136 | stats.totalStars = user.repositories.nodes.reduce((prev, curr) => { 137 | return prev + curr.stargazers.totalCount; 138 | }, 0); 139 | 140 | stats.rank = calculateRank({ 141 | totalCommits: stats.totalCommits, 142 | totalRepos: user.repositories.totalCount, 143 | followers: user.followers.totalCount, 144 | contributions: stats.contributedTo, 145 | stargazers: stats.totalStars, 146 | prs: stats.totalPRs, 147 | issues: stats.totalIssues, 148 | }); 149 | 150 | return stats; 151 | } 152 | 153 | module.exports = fetchStats; 154 | -------------------------------------------------------------------------------- /src/fetchers/top-languages-fetcher.js: -------------------------------------------------------------------------------- 1 | const { request, logger, clampValue } = require("../common/utils"); 2 | const retryer = require("../common/retryer"); 3 | require("dotenv").config(); 4 | 5 | const fetcher = (variables, token) => { 6 | return request( 7 | { 8 | query: ` 9 | query userInfo($login: String!) { 10 | user(login: $login) { 11 | # fetch only owner repos & not forks 12 | repositories(ownerAffiliations: OWNER, isFork: false, first: 100) { 13 | nodes { 14 | name 15 | languages(first: 10, orderBy: {field: SIZE, direction: DESC}) { 16 | edges { 17 | size 18 | node { 19 | color 20 | name 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | `, 29 | variables, 30 | }, 31 | { 32 | Authorization: `bearer ${token}`, 33 | }, 34 | ); 35 | }; 36 | 37 | async function fetchTopLanguages(username, langsCount = 5, exclude_repo = []) { 38 | if (!username) throw Error("Invalid username"); 39 | 40 | langsCount = clampValue(parseInt(langsCount), 1, 10); 41 | 42 | const res = await retryer(fetcher, { login: username }); 43 | 44 | if (res.data.errors) { 45 | logger.error(res.data.errors); 46 | throw Error(res.data.errors[0].message || "Could not fetch user"); 47 | } 48 | 49 | let repoNodes = res.data.data.user.repositories.nodes; 50 | let repoToHide = {}; 51 | 52 | // populate repoToHide map for quick lookup 53 | // while filtering out 54 | if (exclude_repo) { 55 | exclude_repo.forEach((repoName) => { 56 | repoToHide[repoName] = true; 57 | }); 58 | } 59 | 60 | // filter out repositories to be hidden 61 | repoNodes = repoNodes 62 | .sort((a, b) => b.size - a.size) 63 | .filter((name) => { 64 | return !repoToHide[name.name]; 65 | }); 66 | 67 | repoNodes = repoNodes 68 | .filter((node) => { 69 | return node.languages.edges.length > 0; 70 | }) 71 | // flatten the list of language nodes 72 | .reduce((acc, curr) => curr.languages.edges.concat(acc), []) 73 | .reduce((acc, prev) => { 74 | // get the size of the language (bytes) 75 | let langSize = prev.size; 76 | 77 | // if we already have the language in the accumulator 78 | // & the current language name is same as previous name 79 | // add the size to the language size. 80 | if (acc[prev.node.name] && prev.node.name === acc[prev.node.name].name) { 81 | langSize = prev.size + acc[prev.node.name].size; 82 | } 83 | return { 84 | ...acc, 85 | [prev.node.name]: { 86 | name: prev.node.name, 87 | color: prev.node.color, 88 | size: langSize, 89 | }, 90 | }; 91 | }, {}); 92 | 93 | const topLangs = Object.keys(repoNodes) 94 | .sort((a, b) => repoNodes[b].size - repoNodes[a].size) 95 | .slice(0, langsCount) 96 | .reduce((result, key) => { 97 | result[key] = repoNodes[key]; 98 | return result; 99 | }, {}); 100 | 101 | return topLangs; 102 | } 103 | 104 | module.exports = fetchTopLanguages; 105 | -------------------------------------------------------------------------------- /src/fetchers/wakatime-fetcher.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | 3 | const fetchWakatimeStats = async ({ username, api_domain }) => { 4 | try { 5 | const { data } = await axios.get( 6 | `https://${ 7 | api_domain ? api_domain.replace(/[^a-z-.0-9]/gi, "") : "wakatime.com" 8 | }/api/v1/users/${username}/stats?is_including_today=true`, 9 | ); 10 | 11 | return data.data; 12 | } catch (err) { 13 | if (err.response.status < 200 || err.response.status > 299) { 14 | throw new Error( 15 | "Wakatime user not found, make sure you have a wakatime profile", 16 | ); 17 | } 18 | throw err; 19 | } 20 | }; 21 | 22 | module.exports = { 23 | fetchWakatimeStats, 24 | }; 25 | -------------------------------------------------------------------------------- /src/getStyles.js: -------------------------------------------------------------------------------- 1 | const calculateCircleProgress = (value) => { 2 | let radius = 40; 3 | let c = Math.PI * (radius * 2); 4 | 5 | if (value < 0) value = 0; 6 | if (value > 100) value = 100; 7 | 8 | let percentage = ((100 - value) / 100) * c; 9 | return percentage; 10 | }; 11 | 12 | const getProgressAnimation = ({ progress }) => { 13 | return ` 14 | @keyframes rankAnimation { 15 | from { 16 | stroke-dashoffset: ${calculateCircleProgress(0)}; 17 | } 18 | to { 19 | stroke-dashoffset: ${calculateCircleProgress(progress)}; 20 | } 21 | } 22 | `; 23 | }; 24 | 25 | const getAnimations = () => { 26 | return ` 27 | /* Animations */ 28 | @keyframes scaleInAnimation { 29 | from { 30 | transform: translate(-5px, 5px) scale(0); 31 | } 32 | to { 33 | transform: translate(-5px, 5px) scale(1); 34 | } 35 | } 36 | @keyframes fadeInAnimation { 37 | from { 38 | opacity: 0; 39 | } 40 | to { 41 | opacity: 1; 42 | } 43 | } 44 | `; 45 | }; 46 | 47 | const getStyles = ({ 48 | titleColor, 49 | textColor, 50 | iconColor, 51 | show_icons, 52 | progress, 53 | }) => { 54 | return ` 55 | .stat { 56 | font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor}; 57 | } 58 | .stagger { 59 | opacity: 0; 60 | animation: fadeInAnimation 0.3s ease-in-out forwards; 61 | } 62 | .rank-text { 63 | font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor}; 64 | animation: scaleInAnimation 0.3s ease-in-out forwards; 65 | } 66 | 67 | .bold { font-weight: 700 } 68 | .icon { 69 | fill: ${iconColor}; 70 | display: ${!!show_icons ? "block" : "none"}; 71 | } 72 | 73 | .rank-circle-rim { 74 | stroke: ${titleColor}; 75 | fill: none; 76 | stroke-width: 6; 77 | opacity: 0.2; 78 | } 79 | .rank-circle { 80 | stroke: ${titleColor}; 81 | stroke-dasharray: 250; 82 | fill: none; 83 | stroke-width: 6; 84 | stroke-linecap: round; 85 | opacity: 0.8; 86 | transform-origin: -10px 8px; 87 | transform: rotate(-90deg); 88 | animation: rankAnimation 1s forwards ease-in-out; 89 | } 90 | ${process.env.NODE_ENV === "test" ? "" : getProgressAnimation({ progress })} 91 | `; 92 | }; 93 | 94 | module.exports = { getStyles, getAnimations }; 95 | -------------------------------------------------------------------------------- /src/translations.js: -------------------------------------------------------------------------------- 1 | const { encodeHTML } = require("./common/utils"); 2 | 3 | const statCardLocales = ({ name, apostrophe }) => { 4 | return { 5 | "statcard.title": { 6 | cn: `${encodeHTML(name)} 的 GitHub 统计`, 7 | cs: `GitHub statistiky uživatele ${encodeHTML(name)}`, 8 | de: `${encodeHTML(name) + apostrophe} GitHub-Statistiken`, 9 | en: `${encodeHTML(name)}'${apostrophe} GitHub Stats`, 10 | es: `Estadísticas de GitHub de ${encodeHTML(name)}`, 11 | fr: `Statistiques GitHub de ${encodeHTML(name)}`, 12 | hu: `${encodeHTML(name)} GitHub statisztika`, 13 | it: `Statistiche GitHub di ${encodeHTML(name)}`, 14 | ja: `${encodeHTML(name)}の GitHub 統計`, 15 | kr: `${encodeHTML(name)}의 GitHub 통계`, 16 | nl: `${encodeHTML(name)}'${apostrophe} GitHub Statistieken`, 17 | "pt-pt": `Estatísticas do GitHub de ${encodeHTML(name)}`, 18 | "pt-br": `Estatísticas do GitHub de ${encodeHTML(name)}`, 19 | np: `${encodeHTML(name)}'${apostrophe} गिटहब तथ्याङ्क`, 20 | el: `Στατιστικά GitHub του ${encodeHTML(name)}`, 21 | ru: `Статистика GitHub пользователя ${encodeHTML(name)}`, 22 | "uk-ua": `Статистика GitHub користувача ${encodeHTML(name)}`, 23 | id: `Statistik GitHub ${encodeHTML(name)}`, 24 | my: `Statistik GitHub ${encodeHTML(name)}`, 25 | sk: `GitHub štatistiky používateľa ${encodeHTML(name)}`, 26 | tr: `${encodeHTML(name)} Hesabının GitHub Yıldızları`, 27 | pl: `Statystyki GitHub użytkownika ${encodeHTML(name)}`, 28 | }, 29 | "statcard.totalstars": { 30 | cn: "获标星(star)", 31 | cs: "Celkem hvězd", 32 | de: "Sterne Insgesamt", 33 | en: "Total Stars", 34 | es: "Estrellas totales", 35 | fr: "Total d'étoiles", 36 | hu: "Csillagok", 37 | it: "Stelle totali", 38 | ja: "スターされた数", 39 | kr: "받은 스타 수", 40 | nl: "Totale Sterren", 41 | "pt-pt": "Total de estrelas", 42 | "pt-br": "Total de estrelas", 43 | np: "कुल ताराहरू", 44 | el: "Σύνολο Αστεριών", 45 | ru: "Всего звезд", 46 | "uk-ua": "Всього зірок", 47 | id: "Total Bintang", 48 | my: "Jumlah Bintang", 49 | sk: "Hviezdy", 50 | tr: "Toplam Yıldız", 51 | pl: "Liczba Gwiazdek", 52 | }, 53 | "statcard.commits": { 54 | cn: "累计提交(commit)", 55 | cs: "Celkem commitů", 56 | de: "Anzahl Commits", 57 | en: "Total Commits", 58 | es: "Compromisos totales", 59 | fr: "Total des validations", 60 | hu: "Összes commit", 61 | it: "Commit totali", 62 | ja: "合計コミット数", 63 | kr: "전체 커밋 수", 64 | nl: "Totale Commits", 65 | "pt-pt": "Total de Commits", 66 | "pt-br": "Total de Commits", 67 | np: "कुल Commits", 68 | el: "Σύνολο Commits", 69 | ru: "Всего коммитов", 70 | "uk-ua": "Всього коммітов", 71 | id: "Total Komitmen", 72 | my: "Jumlah Komitmen", 73 | sk: "Všetky commity", 74 | tr: "Toplam Commit", 75 | pl: "Wszystkie commity", 76 | }, 77 | "statcard.prs": { 78 | cn: "提案数(PR)", 79 | cs: "Celkem PRs", 80 | de: "PRs Insgesamt", 81 | en: "Total PRs", 82 | es: "RP totales", 83 | fr: "Total des PR", 84 | hu: "Összes PR", 85 | it: "PR totali", 86 | ja: "合計 PR", 87 | kr: "PR 횟수", 88 | nl: "Totale PR's", 89 | "pt-pt": "Total de PRs", 90 | "pt-br": "Total de PRs", 91 | np: "कुल PRs", 92 | el: "Σύνολο PRs", 93 | ru: "Всего pull request`ов", 94 | "uk-ua": "Всього pull request`iв", 95 | id: "Total Permintaan Tarik", 96 | my: "Jumlah PR", 97 | sk: "Všetky PR", 98 | tr: "Toplam PR", 99 | pl: "Wszystkie PR", 100 | }, 101 | "statcard.issues": { 102 | cn: "指出问题(issue)", 103 | cs: "Celkem problémů", 104 | de: "Anzahl Issues", 105 | en: "Total Issues", 106 | es: "Problemas totales", 107 | fr: "Nombre total d'incidents", 108 | hu: "Összes hibajegy", 109 | it: "Segnalazioni totali", 110 | ja: "合計 issue", 111 | kr: "이슈 개수", 112 | nl: "Totale Issues", 113 | "pt-pt": "Total de Issues", 114 | "pt-br": "Total de Issues", 115 | np: "कुल मुद्दाहरू", 116 | el: "Σύνολο Ζητημάτων", 117 | ru: "Всего issue", 118 | "uk-ua": "Всього issue", 119 | id: "Total Masalah Dilaporkan", 120 | my: "Jumlah Isu Dilaporkan", 121 | sk: "Všetky problémy", 122 | tr: "Toplam Hata", 123 | pl: "Wszystkie problemy", 124 | }, 125 | "statcard.contribs": { 126 | cn: "参与项目数", 127 | cs: "Přispěl k", 128 | de: "Beigetragen zu", 129 | en: "Contributed to", 130 | es: "Contribuido a", 131 | fr: "Contribué à", 132 | hu: "Hozzájárulások", 133 | it: "Ha contribuito a", 134 | ja: "コントリビュートしたリポジトリ", 135 | kr: "전체 기여도", 136 | nl: "Bijgedragen aan", 137 | "pt-pt": "Contribuiu em", 138 | "pt-br": "Contribuiu para", 139 | np: "कुल योगदानहरू", 140 | el: "Συνεισφέρθηκε σε", 141 | ru: "Внёс вклад в", 142 | "uk-ua": "Вніс внесок у", 143 | id: "Berkontribusi ke", 144 | my: "Menyumbang kepada", 145 | sk: "Účasti", 146 | tr: "Katkı Verildi", 147 | pl: "Udziały", 148 | }, 149 | }; 150 | }; 151 | 152 | const repoCardLocales = { 153 | "repocard.template": { 154 | cn: "模板", 155 | cs: "Šablona", 156 | de: "Vorlage", 157 | en: "Template", 158 | es: "Modelo", 159 | fr: "Modèle", 160 | hu: "Sablon", 161 | it: "Template", 162 | ja: "テンプレート", 163 | kr: "템플릿", 164 | nl: "Sjabloon", 165 | "pt-pt": "Modelo", 166 | "pt-br": "Modelo", 167 | np: "टेम्पलेट", 168 | el: "Πρότυπο", 169 | ru: "Шаблон", 170 | "uk-ua": "Шаблон", 171 | id: "Pola", 172 | my: "Templat", 173 | sk: "Šablóna", 174 | tr: "Şablon", 175 | pl: "Szablony", 176 | }, 177 | "repocard.archived": { 178 | cn: "已归档", 179 | cs: "Archivováno", 180 | de: "Archiviert", 181 | en: "Archived", 182 | es: "Archivé", 183 | fr: "Archivé", 184 | hu: "Archivált", 185 | it: "Archiviata", 186 | ja: "アーカイブ済み", 187 | kr: "보관됨", 188 | nl: "Gearchiveerd", 189 | "pt-pt": "Arquivados", 190 | "pt-br": "Arquivados", 191 | np: "अभिलेख राखियो", 192 | el: "Αρχειοθετημένα", 193 | ru: "Архивирован", 194 | "uk-ua": "Архивирован", 195 | id: "Arsip", 196 | my: "Arkib", 197 | sk: "Archivované", 198 | tr: "Arşiv", 199 | pl: "Zarchiwizowano", 200 | }, 201 | }; 202 | 203 | const langCardLocales = { 204 | "langcard.title": { 205 | cn: "最常用的语言", 206 | cs: "Nejpoužívanější jazyky", 207 | de: "Meist verwendete Sprachen", 208 | en: "Most Used Languages", 209 | es: "Idiomas más usados", 210 | fr: "Langages les plus utilisés", 211 | hu: "Leggyakrabban használt nyelvek", 212 | it: "Linguaggi più utilizzati", 213 | ja: "最もよく使っている言語", 214 | kr: "가장 많이 사용된 언어", 215 | nl: "Meest gebruikte talen", 216 | "pt-pt": "Idiomas mais usados", 217 | "pt-br": "Linguagens mais usadas", 218 | np: "अधिक प्रयोग गरिएको भाषाहरू", 219 | el: "Οι περισσότερο χρησιμοποιούμενες γλώσσες", 220 | ru: "Наиболее часто используемые языки", 221 | "uk-ua": "Найбільш часто використовувані мови", 222 | id: "Bahasa Yang Paling Banyak Digunakan", 223 | my: "Bahasa Paling Digunakan", 224 | sk: "Najviac používané jazyky", 225 | tr: "En Çok Kullanılan Diller", 226 | pl: "Najczęściej używane języki", 227 | }, 228 | }; 229 | 230 | const wakatimeCardLocales = { 231 | "wakatimecard.title": { 232 | cn: "Wakatime 周统计", 233 | cs: "Statistiky Wakatime", 234 | de: "Wakatime Status", 235 | en: "Wakatime Stats", 236 | es: "Estadísticas de Wakatime", 237 | fr: "Statistiques de Wakatime", 238 | hu: "Wakatime statisztika", 239 | it: "Statistiche Wakatime", 240 | ja: "Wakatime ワカタイム統計", 241 | kr: "Wakatime 주간 통계", 242 | nl: "Takwimu za Wakatime", 243 | "pt-pt": "Estatísticas Wakatime", 244 | "pt-br": "Estatísticas Wakatime", 245 | np: "Wakatime तथ्या .्क", 246 | el: "Στατιστικά Wakatime", 247 | ru: "Статистика Вакатиме", 248 | "uk-ua": "Статистика Wakatime", 249 | id: "Status Wakatime", 250 | my: "Statistik Wakatime", 251 | sk: "Wakatime štatistika", 252 | tr: "Waketime İstatistikler", 253 | pl: "statystyki Wakatime", 254 | }, 255 | "wakatimecard.nocodingactivity": { 256 | cn: "本周没有编程活动", 257 | cs: "Tento týden žádná aktivita v kódování", 258 | de: "Keine Aktivitäten in dieser Woche", 259 | en: "No coding activity this week", 260 | es: "No hay actividad de codificación esta semana", 261 | fr: "Aucune activité de codage cette semaine", 262 | hu: "Nem volt aktivitás ezen a héten", 263 | it: "Nessuna attività in questa settimana", 264 | ja: "今週のコーディング活動はありません", 265 | kr: "이번 주 작업내역 없음", 266 | nl: "Geen coderings activiet deze week", 267 | "pt-pt": "Sem atividade esta semana", 268 | "pt-br": "Nenhuma atividade de codificação esta semana", 269 | np: "यस हप्ता कुनै कोडिंग गतिविधि छैन", 270 | el: "Δεν υπάρχει δραστηριότητα κώδικα γι' αυτή την εβδομάδα", 271 | ru: "На этой неделе не было активности", 272 | "uk-ua": "На цьому тижні не було активності", 273 | id: "Tidak ada aktivitas perkodingan minggu ini", 274 | my: "Tiada aktiviti pengekodan minggu ini", 275 | sk: "Žiadna kódovacia aktivita tento týždeň", 276 | tr: "Bu hafta herhangi bir kod yazma aktivitesi olmadı", 277 | pl: "Brak aktywności w tym tygodniu", 278 | }, 279 | }; 280 | 281 | const availableLocales = Object.keys(repoCardLocales["repocard.archived"]); 282 | 283 | function isLocaleAvailable(locale) { 284 | return availableLocales.includes(locale.toLowerCase()); 285 | } 286 | 287 | module.exports = { 288 | isLocaleAvailable, 289 | availableLocales, 290 | statCardLocales, 291 | repoCardLocales, 292 | langCardLocales, 293 | wakatimeCardLocales, 294 | }; 295 | -------------------------------------------------------------------------------- /tests/__snapshots__/renderWakatimeCard.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Test Render Wakatime Card should render correctly 1`] = ` 4 | " 5 | 12 | 63 | 64 | undefined 65 | 66 | 77 | 78 | 79 | 83 | 84 | Wakatime Stats 90 | 91 | 92 | 93 | 94 | 98 | 99 | 100 | 101 | 102 | Other: 103 | 19 mins 109 | 110 | 111 | 112 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | TypeScript: 126 | 1 min 132 | 133 | 134 | 135 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | " 152 | `; 153 | 154 | exports[`Test Render Wakatime Card should render correctly with compact layout 1`] = ` 155 | " 156 | 163 | 214 | 215 | undefined 216 | 217 | 228 | 229 | 230 | 234 | 235 | Wakatime Stats 241 | 242 | 243 | 244 | 245 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 265 | 266 | 275 | 276 | 285 | 286 | 287 | 288 | 289 | 290 | Other - 19 mins 291 | 292 | 293 | 294 | 295 | 296 | 297 | TypeScript - 1 min 298 | 299 | 300 | 301 | 302 | 303 | 304 | YAML - 0 secs 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | " 314 | `; 315 | -------------------------------------------------------------------------------- /tests/api.test.js: -------------------------------------------------------------------------------- 1 | require("@testing-library/jest-dom"); 2 | const axios = require("axios"); 3 | const MockAdapter = require("axios-mock-adapter"); 4 | const api = require("../api/index"); 5 | const renderStatsCard = require("../src/cards/stats-card"); 6 | const { renderError, CONSTANTS } = require("../src/common/utils"); 7 | const calculateRank = require("../src/calculateRank"); 8 | 9 | const stats = { 10 | name: "Anurag Hazra", 11 | totalStars: 100, 12 | totalCommits: 200, 13 | totalIssues: 300, 14 | totalPRs: 400, 15 | contributedTo: 500, 16 | rank: null, 17 | }; 18 | stats.rank = calculateRank({ 19 | totalCommits: stats.totalCommits, 20 | totalRepos: 1, 21 | followers: 0, 22 | contributions: stats.contributedTo, 23 | stargazers: stats.totalStars, 24 | prs: stats.totalPRs, 25 | issues: stats.totalIssues, 26 | }); 27 | 28 | const data = { 29 | data: { 30 | user: { 31 | name: stats.name, 32 | repositoriesContributedTo: { totalCount: stats.contributedTo }, 33 | contributionsCollection: { 34 | totalCommitContributions: stats.totalCommits, 35 | restrictedContributionsCount: 100, 36 | }, 37 | pullRequests: { totalCount: stats.totalPRs }, 38 | issues: { totalCount: stats.totalIssues }, 39 | followers: { totalCount: 0 }, 40 | repositories: { 41 | totalCount: 1, 42 | nodes: [{ stargazers: { totalCount: 100 } }], 43 | }, 44 | }, 45 | }, 46 | }; 47 | 48 | const error = { 49 | errors: [ 50 | { 51 | type: "NOT_FOUND", 52 | path: ["user"], 53 | locations: [], 54 | message: "Could not fetch user", 55 | }, 56 | ], 57 | }; 58 | 59 | const mock = new MockAdapter(axios); 60 | 61 | const faker = (query, data) => { 62 | const req = { 63 | query: { 64 | username: "SrGobi", 65 | ...query, 66 | }, 67 | }; 68 | const res = { 69 | setHeader: jest.fn(), 70 | send: jest.fn(), 71 | }; 72 | mock.onPost("https://api.github.com/graphql").reply(200, data); 73 | 74 | return { req, res }; 75 | }; 76 | 77 | afterEach(() => { 78 | mock.reset(); 79 | }); 80 | 81 | describe("Test /api/", () => { 82 | it("should test the request", async () => { 83 | const { req, res } = faker({}, data); 84 | 85 | await api(req, res); 86 | 87 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml"); 88 | expect(res.send).toBeCalledWith(renderStatsCard(stats, { ...req.query })); 89 | }); 90 | 91 | it("should render error card on error", async () => { 92 | const { req, res } = faker({}, error); 93 | 94 | await api(req, res); 95 | 96 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml"); 97 | expect(res.send).toBeCalledWith( 98 | renderError( 99 | error.errors[0].message, 100 | "Make sure the provided username is not an organization", 101 | ), 102 | ); 103 | }); 104 | 105 | it("should get the query options", async () => { 106 | const { req, res } = faker( 107 | { 108 | username: "SrGobi", 109 | hide: "issues,prs,contribs", 110 | show_icons: true, 111 | hide_border: true, 112 | line_height: 100, 113 | title_color: "fff", 114 | icon_color: "fff", 115 | text_color: "fff", 116 | bg_color: "fff", 117 | }, 118 | data, 119 | ); 120 | 121 | await api(req, res); 122 | 123 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml"); 124 | expect(res.send).toBeCalledWith( 125 | renderStatsCard(stats, { 126 | hide: ["issues", "prs", "contribs"], 127 | show_icons: true, 128 | hide_border: true, 129 | line_height: 100, 130 | title_color: "fff", 131 | icon_color: "fff", 132 | text_color: "fff", 133 | bg_color: "fff", 134 | }), 135 | ); 136 | }); 137 | 138 | it("should have proper cache", async () => { 139 | const { req, res } = faker({}, data); 140 | mock.onPost("https://api.github.com/graphql").reply(200, data); 141 | 142 | await api(req, res); 143 | 144 | expect(res.setHeader.mock.calls).toEqual([ 145 | ["Content-Type", "image/svg+xml"], 146 | ["Cache-Control", `public, max-age=${CONSTANTS.TWO_HOURS}`], 147 | ]); 148 | }); 149 | 150 | it("should set proper cache", async () => { 151 | const { req, res } = faker({ cache_seconds: 8000 }, data); 152 | await api(req, res); 153 | 154 | expect(res.setHeader.mock.calls).toEqual([ 155 | ["Content-Type", "image/svg+xml"], 156 | ["Cache-Control", `public, max-age=${8000}`], 157 | ]); 158 | }); 159 | 160 | it("should set proper cache with clamped values", async () => { 161 | { 162 | let { req, res } = faker({ cache_seconds: 200000 }, data); 163 | await api(req, res); 164 | 165 | expect(res.setHeader.mock.calls).toEqual([ 166 | ["Content-Type", "image/svg+xml"], 167 | ["Cache-Control", `public, max-age=${CONSTANTS.ONE_DAY}`], 168 | ]); 169 | } 170 | 171 | // note i'm using block scoped vars 172 | { 173 | let { req, res } = faker({ cache_seconds: 0 }, data); 174 | await api(req, res); 175 | 176 | expect(res.setHeader.mock.calls).toEqual([ 177 | ["Content-Type", "image/svg+xml"], 178 | ["Cache-Control", `public, max-age=${CONSTANTS.TWO_HOURS}`], 179 | ]); 180 | } 181 | 182 | { 183 | let { req, res } = faker({ cache_seconds: -10000 }, data); 184 | await api(req, res); 185 | 186 | expect(res.setHeader.mock.calls).toEqual([ 187 | ["Content-Type", "image/svg+xml"], 188 | ["Cache-Control", `public, max-age=${CONSTANTS.TWO_HOURS}`], 189 | ]); 190 | } 191 | }); 192 | 193 | it("should add private contributions", async () => { 194 | const { req, res } = faker( 195 | { 196 | username: "SrGobi", 197 | count_private: true, 198 | }, 199 | data, 200 | ); 201 | 202 | await api(req, res); 203 | 204 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml"); 205 | expect(res.send).toBeCalledWith( 206 | renderStatsCard( 207 | { 208 | ...stats, 209 | totalCommits: stats.totalCommits + 100, 210 | rank: calculateRank({ 211 | totalCommits: stats.totalCommits + 100, 212 | totalRepos: 1, 213 | followers: 0, 214 | contributions: stats.contributedTo, 215 | stargazers: stats.totalStars, 216 | prs: stats.totalPRs, 217 | issues: stats.totalIssues, 218 | }), 219 | }, 220 | {}, 221 | ), 222 | ); 223 | }); 224 | }); 225 | -------------------------------------------------------------------------------- /tests/calculateRank.test.js: -------------------------------------------------------------------------------- 1 | require("@testing-library/jest-dom"); 2 | const calculateRank = require("../src/calculateRank"); 3 | 4 | describe("Test calculateRank", () => { 5 | it("should calculate rank correctly", () => { 6 | expect( 7 | calculateRank({ 8 | totalCommits: 100, 9 | totalRepos: 5, 10 | followers: 100, 11 | contributions: 61, 12 | stargazers: 400, 13 | prs: 300, 14 | issues: 200, 15 | }), 16 | ).toStrictEqual({ level: "A+", score: 49.16605417270399 }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/card.test.js: -------------------------------------------------------------------------------- 1 | require("@testing-library/jest-dom"); 2 | const cssToObject = require("css-to-object"); 3 | const Card = require("../src/common/Card"); 4 | const icons = require("../src/common/icons"); 5 | const { getCardColors } = require("../src/common/utils"); 6 | const { queryByTestId } = require("@testing-library/dom"); 7 | 8 | describe("Card", () => { 9 | it("should hide border", () => { 10 | const card = new Card({}); 11 | card.setHideBorder(true); 12 | 13 | document.body.innerHTML = card.render(``); 14 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute( 15 | "stroke-opacity", 16 | "0", 17 | ); 18 | }); 19 | 20 | it("should not hide border", () => { 21 | const card = new Card({}); 22 | card.setHideBorder(false); 23 | 24 | document.body.innerHTML = card.render(``); 25 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute( 26 | "stroke-opacity", 27 | "1", 28 | ); 29 | }); 30 | 31 | it("should have a custom title", () => { 32 | const card = new Card({ 33 | customTitle: "custom title", 34 | defaultTitle: "default title", 35 | }); 36 | 37 | document.body.innerHTML = card.render(``); 38 | expect(queryByTestId(document.body, "card-title")).toHaveTextContent( 39 | "custom title", 40 | ); 41 | }); 42 | 43 | it("should hide title", () => { 44 | const card = new Card({}); 45 | card.setHideTitle(true); 46 | 47 | document.body.innerHTML = card.render(``); 48 | expect(queryByTestId(document.body, "card-title")).toBeNull(); 49 | }); 50 | 51 | it("should not hide title", () => { 52 | const card = new Card({}); 53 | card.setHideTitle(false); 54 | 55 | document.body.innerHTML = card.render(``); 56 | expect(queryByTestId(document.body, "card-title")).toBeInTheDocument(); 57 | }); 58 | 59 | it("title should have prefix icon", () => { 60 | const card = new Card({ title: "ok", titlePrefixIcon: icons.contribs }); 61 | 62 | document.body.innerHTML = card.render(``); 63 | expect(document.getElementsByClassName("icon")[0]).toBeInTheDocument(); 64 | }); 65 | 66 | it("title should not have prefix icon", () => { 67 | const card = new Card({ title: "ok" }); 68 | 69 | document.body.innerHTML = card.render(``); 70 | expect(document.getElementsByClassName("icon")[0]).toBeUndefined(); 71 | }); 72 | 73 | it("should have proper height, width", () => { 74 | const card = new Card({ height: 200, width: 200, title: "ok" }); 75 | document.body.innerHTML = card.render(``); 76 | expect(document.getElementsByTagName("svg")[0]).toHaveAttribute( 77 | "height", 78 | "200", 79 | ); 80 | expect(document.getElementsByTagName("svg")[0]).toHaveAttribute( 81 | "height", 82 | "200", 83 | ); 84 | }); 85 | 86 | it("should have less height after title is hidden", () => { 87 | const card = new Card({ height: 200, title: "ok" }); 88 | card.setHideTitle(true); 89 | 90 | document.body.innerHTML = card.render(``); 91 | expect(document.getElementsByTagName("svg")[0]).toHaveAttribute( 92 | "height", 93 | "170", 94 | ); 95 | }); 96 | 97 | it("main-card-body should have proper when title is visible", () => { 98 | const card = new Card({ height: 200 }); 99 | document.body.innerHTML = card.render(``); 100 | expect(queryByTestId(document.body, "main-card-body")).toHaveAttribute( 101 | "transform", 102 | "translate(0, 55)", 103 | ); 104 | }); 105 | 106 | it("main-card-body should have proper position after title is hidden", () => { 107 | const card = new Card({ height: 200 }); 108 | card.setHideTitle(true); 109 | 110 | document.body.innerHTML = card.render(``); 111 | expect(queryByTestId(document.body, "main-card-body")).toHaveAttribute( 112 | "transform", 113 | "translate(0, 25)", 114 | ); 115 | }); 116 | 117 | it("should render with correct colors", () => { 118 | // returns theme based colors with proper overrides and defaults 119 | const { titleColor, textColor, iconColor, bgColor } = getCardColors({ 120 | title_color: "f00", 121 | icon_color: "0f0", 122 | text_color: "00f", 123 | bg_color: "fff", 124 | theme: "default", 125 | }); 126 | 127 | const card = new Card({ 128 | height: 200, 129 | colors: { 130 | titleColor, 131 | textColor, 132 | iconColor, 133 | bgColor, 134 | }, 135 | }); 136 | document.body.innerHTML = card.render(``); 137 | 138 | const styleTag = document.querySelector("style"); 139 | const stylesObject = cssToObject(styleTag.innerHTML); 140 | const headerClassStyles = stylesObject[".header"]; 141 | 142 | expect(headerClassStyles.fill).toBe("#f00"); 143 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute( 144 | "fill", 145 | "#fff", 146 | ); 147 | }); 148 | it("should render gradient backgrounds", () => { 149 | const { titleColor, textColor, iconColor, bgColor } = getCardColors({ 150 | title_color: "f00", 151 | icon_color: "0f0", 152 | text_color: "00f", 153 | bg_color: "90,fff,000,f00", 154 | theme: "default", 155 | }); 156 | 157 | const card = new Card({ 158 | height: 200, 159 | colors: { 160 | titleColor, 161 | textColor, 162 | iconColor, 163 | bgColor, 164 | }, 165 | }); 166 | document.body.innerHTML = card.render(``); 167 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute( 168 | "fill", 169 | "url(#gradient)", 170 | ); 171 | expect(document.querySelector("defs linearGradient")).toHaveAttribute( 172 | "gradientTransform", 173 | "rotate(90)", 174 | ); 175 | expect( 176 | document.querySelector("defs linearGradient stop:nth-child(1)"), 177 | ).toHaveAttribute("stop-color", "#fff"); 178 | expect( 179 | document.querySelector("defs linearGradient stop:nth-child(2)"), 180 | ).toHaveAttribute("stop-color", "#000"); 181 | expect( 182 | document.querySelector("defs linearGradient stop:nth-child(3)"), 183 | ).toHaveAttribute("stop-color", "#f00"); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /tests/fetchRepo.test.js: -------------------------------------------------------------------------------- 1 | require("@testing-library/jest-dom"); 2 | const axios = require("axios"); 3 | const MockAdapter = require("axios-mock-adapter"); 4 | const fetchRepo = require("../src/fetchers/repo-fetcher"); 5 | 6 | const data_repo = { 7 | repository: { 8 | name: "convoychat", 9 | stargazers: { totalCount: 38000 }, 10 | description: "Help us take over the world! React + TS + GraphQL Chat App", 11 | primaryLanguage: { 12 | color: "#2b7489", 13 | id: "MDg6TGFuZ3VhZ2UyODc=", 14 | name: "TypeScript", 15 | }, 16 | forkCount: 100, 17 | }, 18 | }; 19 | 20 | const data_user = { 21 | data: { 22 | user: { repository: data_repo }, 23 | organization: null, 24 | }, 25 | }; 26 | const data_org = { 27 | data: { 28 | user: null, 29 | organization: { repository: data_repo }, 30 | }, 31 | }; 32 | 33 | const mock = new MockAdapter(axios); 34 | 35 | afterEach(() => { 36 | mock.reset(); 37 | }); 38 | 39 | describe("Test fetchRepo", () => { 40 | it("should fetch correct user repo", async () => { 41 | mock.onPost("https://api.github.com/graphql").reply(200, data_user); 42 | 43 | let repo = await fetchRepo("SrGobi", "convoychat"); 44 | expect(repo).toStrictEqual(data_repo); 45 | }); 46 | 47 | it("should fetch correct org repo", async () => { 48 | mock.onPost("https://api.github.com/graphql").reply(200, data_org); 49 | 50 | let repo = await fetchRepo("SrGobi", "convoychat"); 51 | expect(repo).toStrictEqual(data_repo); 52 | }); 53 | 54 | it("should throw error if user is found but repo is null", async () => { 55 | mock 56 | .onPost("https://api.github.com/graphql") 57 | .reply(200, { data: { user: { repository: null }, organization: null } }); 58 | 59 | await expect(fetchRepo("SrGobi", "convoychat")).rejects.toThrow( 60 | "User Repository Not found", 61 | ); 62 | }); 63 | 64 | it("should throw error if org is found but repo is null", async () => { 65 | mock 66 | .onPost("https://api.github.com/graphql") 67 | .reply(200, { data: { user: null, organization: { repository: null } } }); 68 | 69 | await expect(fetchRepo("SrGobi", "convoychat")).rejects.toThrow( 70 | "Organization Repository Not found", 71 | ); 72 | }); 73 | 74 | it("should throw error if both user & org data not found", async () => { 75 | mock 76 | .onPost("https://api.github.com/graphql") 77 | .reply(200, { data: { user: null, organization: null } }); 78 | 79 | await expect(fetchRepo("SrGobi", "convoychat")).rejects.toThrow( 80 | "Not found", 81 | ); 82 | }); 83 | 84 | it("should throw error if repository is private", async () => { 85 | mock.onPost("https://api.github.com/graphql").reply(200, { 86 | data: { 87 | user: { repository: { ...data_repo, isPrivate: true } }, 88 | organization: null, 89 | }, 90 | }); 91 | 92 | await expect(fetchRepo("SrGobi", "convoychat")).rejects.toThrow( 93 | "User Repository Not found", 94 | ); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /tests/fetchStats.test.js: -------------------------------------------------------------------------------- 1 | require("@testing-library/jest-dom"); 2 | const axios = require("axios"); 3 | const MockAdapter = require("axios-mock-adapter"); 4 | const fetchStats = require("../src/fetchers/stats-fetcher"); 5 | const calculateRank = require("../src/calculateRank"); 6 | 7 | const data = { 8 | data: { 9 | user: { 10 | name: "Anurag Hazra", 11 | repositoriesContributedTo: { totalCount: 61 }, 12 | contributionsCollection: { 13 | totalCommitContributions: 100, 14 | restrictedContributionsCount: 50, 15 | }, 16 | pullRequests: { totalCount: 300 }, 17 | issues: { totalCount: 200 }, 18 | followers: { totalCount: 100 }, 19 | repositories: { 20 | totalCount: 5, 21 | nodes: [ 22 | { stargazers: { totalCount: 100 } }, 23 | { stargazers: { totalCount: 100 } }, 24 | { stargazers: { totalCount: 100 } }, 25 | { stargazers: { totalCount: 50 } }, 26 | { stargazers: { totalCount: 50 } }, 27 | ], 28 | }, 29 | }, 30 | }, 31 | }; 32 | 33 | const error = { 34 | errors: [ 35 | { 36 | type: "NOT_FOUND", 37 | path: ["user"], 38 | locations: [], 39 | message: "Could not resolve to a User with the login of 'noname'.", 40 | }, 41 | ], 42 | }; 43 | 44 | const mock = new MockAdapter(axios); 45 | 46 | afterEach(() => { 47 | mock.reset(); 48 | }); 49 | 50 | describe("Test fetchStats", () => { 51 | it("should fetch correct stats", async () => { 52 | mock.onPost("https://api.github.com/graphql").reply(200, data); 53 | 54 | let stats = await fetchStats("SrGobi"); 55 | const rank = calculateRank({ 56 | totalCommits: 100, 57 | totalRepos: 5, 58 | followers: 100, 59 | contributions: 61, 60 | stargazers: 400, 61 | prs: 300, 62 | issues: 200, 63 | }); 64 | 65 | expect(stats).toStrictEqual({ 66 | contributedTo: 61, 67 | name: "Anurag Hazra", 68 | totalCommits: 100, 69 | totalIssues: 200, 70 | totalPRs: 300, 71 | totalStars: 400, 72 | rank, 73 | }); 74 | }); 75 | 76 | it("should throw error", async () => { 77 | mock.onPost("https://api.github.com/graphql").reply(200, error); 78 | 79 | await expect(fetchStats("SrGobi")).rejects.toThrow( 80 | "Could not resolve to a User with the login of 'noname'.", 81 | ); 82 | }); 83 | 84 | it("should fetch and add private contributions", async () => { 85 | mock.onPost("https://api.github.com/graphql").reply(200, data); 86 | 87 | let stats = await fetchStats("SrGobi", true); 88 | const rank = calculateRank({ 89 | totalCommits: 150, 90 | totalRepos: 5, 91 | followers: 100, 92 | contributions: 61, 93 | stargazers: 400, 94 | prs: 300, 95 | issues: 200, 96 | }); 97 | 98 | expect(stats).toStrictEqual({ 99 | contributedTo: 61, 100 | name: "Anurag Hazra", 101 | totalCommits: 150, 102 | totalIssues: 200, 103 | totalPRs: 300, 104 | totalStars: 400, 105 | rank, 106 | }); 107 | }); 108 | 109 | it("should fetch total commits", async () => { 110 | mock.onPost("https://api.github.com/graphql").reply(200, data); 111 | mock 112 | .onGet("https://api.github.com/search/commits?q=author:SrGobi") 113 | .reply(200, { total_count: 1000 }); 114 | 115 | let stats = await fetchStats("SrGobi", true, true); 116 | const rank = calculateRank({ 117 | totalCommits: 1050, 118 | totalRepos: 5, 119 | followers: 100, 120 | contributions: 61, 121 | stargazers: 400, 122 | prs: 300, 123 | issues: 200, 124 | }); 125 | 126 | expect(stats).toStrictEqual({ 127 | contributedTo: 61, 128 | name: "Anurag Hazra", 129 | totalCommits: 1050, 130 | totalIssues: 200, 131 | totalPRs: 300, 132 | totalStars: 400, 133 | rank, 134 | }); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /tests/fetchTopLanguages.test.js: -------------------------------------------------------------------------------- 1 | require("@testing-library/jest-dom"); 2 | const axios = require("axios"); 3 | const MockAdapter = require("axios-mock-adapter"); 4 | const fetchTopLanguages = require("../src/fetchers/top-languages-fetcher"); 5 | 6 | const mock = new MockAdapter(axios); 7 | 8 | afterEach(() => { 9 | mock.reset(); 10 | }); 11 | 12 | const data_langs = { 13 | data: { 14 | user: { 15 | repositories: { 16 | nodes: [ 17 | { 18 | languages: { 19 | edges: [{ size: 100, node: { color: "#0f0", name: "HTML" } }], 20 | }, 21 | }, 22 | { 23 | languages: { 24 | edges: [{ size: 100, node: { color: "#0f0", name: "HTML" } }], 25 | }, 26 | }, 27 | { 28 | languages: { 29 | edges: [ 30 | { size: 100, node: { color: "#0ff", name: "javascript" } }, 31 | ], 32 | }, 33 | }, 34 | { 35 | languages: { 36 | edges: [ 37 | { size: 100, node: { color: "#0ff", name: "javascript" } }, 38 | ], 39 | }, 40 | }, 41 | ], 42 | }, 43 | }, 44 | }, 45 | }; 46 | 47 | const error = { 48 | errors: [ 49 | { 50 | type: "NOT_FOUND", 51 | path: ["user"], 52 | locations: [], 53 | message: "Could not resolve to a User with the login of 'noname'.", 54 | }, 55 | ], 56 | }; 57 | 58 | describe("FetchTopLanguages", () => { 59 | it("should fetch correct language data", async () => { 60 | mock.onPost("https://api.github.com/graphql").reply(200, data_langs); 61 | 62 | let repo = await fetchTopLanguages("SrGobi"); 63 | expect(repo).toStrictEqual({ 64 | HTML: { 65 | color: "#0f0", 66 | name: "HTML", 67 | size: 200, 68 | }, 69 | javascript: { 70 | color: "#0ff", 71 | name: "javascript", 72 | size: 200, 73 | }, 74 | }); 75 | }); 76 | 77 | it("should fetch langs with specified langs_count", async () => { 78 | mock.onPost("https://api.github.com/graphql").reply(200, data_langs); 79 | 80 | let repo = await fetchTopLanguages("SrGobi", 1); 81 | expect(repo).toStrictEqual({ 82 | javascript: { 83 | color: "#0ff", 84 | name: "javascript", 85 | size: 200, 86 | }, 87 | }); 88 | }); 89 | 90 | it("should throw error", async () => { 91 | mock.onPost("https://api.github.com/graphql").reply(200, error); 92 | 93 | await expect(fetchTopLanguages("SrGobi")).rejects.toThrow( 94 | "Could not resolve to a User with the login of 'noname'.", 95 | ); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /tests/fetchWakatime.test.js: -------------------------------------------------------------------------------- 1 | require("@testing-library/jest-dom"); 2 | const axios = require("axios"); 3 | const MockAdapter = require("axios-mock-adapter"); 4 | const { fetchWakatimeStats } = require("../src/fetchers/wakatime-fetcher"); 5 | const mock = new MockAdapter(axios); 6 | 7 | afterEach(() => { 8 | mock.reset(); 9 | }); 10 | 11 | const wakaTimeData = { 12 | data: { 13 | categories: [ 14 | { 15 | digital: "22:40", 16 | hours: 22, 17 | minutes: 40, 18 | name: "Coding", 19 | percent: 100, 20 | text: "22 hrs 40 mins", 21 | total_seconds: 81643.570077, 22 | }, 23 | ], 24 | daily_average: 16095, 25 | daily_average_including_other_language: 16329, 26 | days_including_holidays: 7, 27 | days_minus_holidays: 5, 28 | editors: [ 29 | { 30 | digital: "22:40", 31 | hours: 22, 32 | minutes: 40, 33 | name: "VS Code", 34 | percent: 100, 35 | text: "22 hrs 40 mins", 36 | total_seconds: 81643.570077, 37 | }, 38 | ], 39 | holidays: 2, 40 | human_readable_daily_average: "4 hrs 28 mins", 41 | human_readable_daily_average_including_other_language: "4 hrs 32 mins", 42 | human_readable_total: "22 hrs 21 mins", 43 | human_readable_total_including_other_language: "22 hrs 40 mins", 44 | id: "random hash", 45 | is_already_updating: false, 46 | is_coding_activity_visible: true, 47 | is_including_today: false, 48 | is_other_usage_visible: true, 49 | is_stuck: false, 50 | is_up_to_date: true, 51 | languages: [ 52 | { 53 | digital: "0:19", 54 | hours: 0, 55 | minutes: 19, 56 | name: "Other", 57 | percent: 1.43, 58 | text: "19 mins", 59 | total_seconds: 1170.434361, 60 | }, 61 | { 62 | digital: "0:01", 63 | hours: 0, 64 | minutes: 1, 65 | name: "TypeScript", 66 | percent: 0.1, 67 | text: "1 min", 68 | total_seconds: 83.293809, 69 | }, 70 | { 71 | digital: "0:00", 72 | hours: 0, 73 | minutes: 0, 74 | name: "YAML", 75 | percent: 0.07, 76 | text: "0 secs", 77 | total_seconds: 54.975151, 78 | }, 79 | ], 80 | operating_systems: [ 81 | { 82 | digital: "22:40", 83 | hours: 22, 84 | minutes: 40, 85 | name: "Mac", 86 | percent: 100, 87 | text: "22 hrs 40 mins", 88 | total_seconds: 81643.570077, 89 | }, 90 | ], 91 | percent_calculated: 100, 92 | range: "last_7_days", 93 | status: "ok", 94 | timeout: 15, 95 | total_seconds: 80473.135716, 96 | total_seconds_including_other_language: 81643.570077, 97 | user_id: "random hash", 98 | username: "SrGobi", 99 | writes_only: false, 100 | }, 101 | }; 102 | 103 | describe("Wakatime fetcher", () => { 104 | it("should fetch correct wakatime data", async () => { 105 | const username = "SrGobi"; 106 | mock 107 | .onGet( 108 | `https://wakatime.com/api/v1/users/${username}/stats?is_including_today=true`, 109 | ) 110 | .reply(200, wakaTimeData); 111 | 112 | const repo = await fetchWakatimeStats({ username }); 113 | expect(repo).toMatchInlineSnapshot(` 114 | Object { 115 | "categories": Array [ 116 | Object { 117 | "digital": "22:40", 118 | "hours": 22, 119 | "minutes": 40, 120 | "name": "Coding", 121 | "percent": 100, 122 | "text": "22 hrs 40 mins", 123 | "total_seconds": 81643.570077, 124 | }, 125 | ], 126 | "daily_average": 16095, 127 | "daily_average_including_other_language": 16329, 128 | "days_including_holidays": 7, 129 | "days_minus_holidays": 5, 130 | "editors": Array [ 131 | Object { 132 | "digital": "22:40", 133 | "hours": 22, 134 | "minutes": 40, 135 | "name": "VS Code", 136 | "percent": 100, 137 | "text": "22 hrs 40 mins", 138 | "total_seconds": 81643.570077, 139 | }, 140 | ], 141 | "holidays": 2, 142 | "human_readable_daily_average": "4 hrs 28 mins", 143 | "human_readable_daily_average_including_other_language": "4 hrs 32 mins", 144 | "human_readable_total": "22 hrs 21 mins", 145 | "human_readable_total_including_other_language": "22 hrs 40 mins", 146 | "id": "random hash", 147 | "is_already_updating": false, 148 | "is_coding_activity_visible": true, 149 | "is_including_today": false, 150 | "is_other_usage_visible": true, 151 | "is_stuck": false, 152 | "is_up_to_date": true, 153 | "languages": Array [ 154 | Object { 155 | "digital": "0:19", 156 | "hours": 0, 157 | "minutes": 19, 158 | "name": "Other", 159 | "percent": 1.43, 160 | "text": "19 mins", 161 | "total_seconds": 1170.434361, 162 | }, 163 | Object { 164 | "digital": "0:01", 165 | "hours": 0, 166 | "minutes": 1, 167 | "name": "TypeScript", 168 | "percent": 0.1, 169 | "text": "1 min", 170 | "total_seconds": 83.293809, 171 | }, 172 | Object { 173 | "digital": "0:00", 174 | "hours": 0, 175 | "minutes": 0, 176 | "name": "YAML", 177 | "percent": 0.07, 178 | "text": "0 secs", 179 | "total_seconds": 54.975151, 180 | }, 181 | ], 182 | "operating_systems": Array [ 183 | Object { 184 | "digital": "22:40", 185 | "hours": 22, 186 | "minutes": 40, 187 | "name": "Mac", 188 | "percent": 100, 189 | "text": "22 hrs 40 mins", 190 | "total_seconds": 81643.570077, 191 | }, 192 | ], 193 | "percent_calculated": 100, 194 | "range": "last_7_days", 195 | "status": "ok", 196 | "timeout": 15, 197 | "total_seconds": 80473.135716, 198 | "total_seconds_including_other_language": 81643.570077, 199 | "user_id": "random hash", 200 | "username": "SrGobi", 201 | "writes_only": false, 202 | } 203 | `); 204 | }); 205 | 206 | it("should throw error", async () => { 207 | mock.onGet(/\/https:\/\/wakatime\.com\/api/).reply(404, wakaTimeData); 208 | 209 | await expect(fetchWakatimeStats("noone")).rejects.toThrow( 210 | "Wakatime user not found, make sure you have a wakatime profile", 211 | ); 212 | }); 213 | }); 214 | 215 | module.exports = { wakaTimeData }; 216 | -------------------------------------------------------------------------------- /tests/pin.test.js: -------------------------------------------------------------------------------- 1 | require("@testing-library/jest-dom"); 2 | const axios = require("axios"); 3 | const MockAdapter = require("axios-mock-adapter"); 4 | const pin = require("../api/pin"); 5 | const renderRepoCard = require("../src/cards/repo-card"); 6 | const { renderError } = require("../src/common/utils"); 7 | 8 | const data_repo = { 9 | repository: { 10 | username: "SrGobi", 11 | name: "convoychat", 12 | stargazers: { totalCount: 38000 }, 13 | description: "Help us take over the world! React + TS + GraphQL Chat App", 14 | primaryLanguage: { 15 | color: "#2b7489", 16 | id: "MDg6TGFuZ3VhZ2UyODc=", 17 | name: "TypeScript", 18 | }, 19 | forkCount: 100, 20 | isTemplate: false, 21 | }, 22 | }; 23 | 24 | const data_user = { 25 | data: { 26 | user: { repository: data_repo.repository }, 27 | organization: null, 28 | }, 29 | }; 30 | 31 | const mock = new MockAdapter(axios); 32 | 33 | afterEach(() => { 34 | mock.reset(); 35 | }); 36 | 37 | describe("Test /api/pin", () => { 38 | it("should test the request", async () => { 39 | const req = { 40 | query: { 41 | username: "SrGobi", 42 | repo: "convoychat", 43 | }, 44 | }; 45 | const res = { 46 | setHeader: jest.fn(), 47 | send: jest.fn(), 48 | }; 49 | mock.onPost("https://api.github.com/graphql").reply(200, data_user); 50 | 51 | await pin(req, res); 52 | 53 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml"); 54 | expect(res.send).toBeCalledWith(renderRepoCard(data_repo.repository)); 55 | }); 56 | 57 | it("should get the query options", async () => { 58 | const req = { 59 | query: { 60 | username: "SrGobi", 61 | repo: "convoychat", 62 | title_color: "fff", 63 | icon_color: "fff", 64 | text_color: "fff", 65 | bg_color: "fff", 66 | full_name: "1", 67 | }, 68 | }; 69 | const res = { 70 | setHeader: jest.fn(), 71 | send: jest.fn(), 72 | }; 73 | mock.onPost("https://api.github.com/graphql").reply(200, data_user); 74 | 75 | await pin(req, res); 76 | 77 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml"); 78 | expect(res.send).toBeCalledWith( 79 | renderRepoCard(data_repo.repository, { ...req.query }), 80 | ); 81 | }); 82 | 83 | it("should render error card if user repo not found", async () => { 84 | const req = { 85 | query: { 86 | username: "SrGobi", 87 | repo: "convoychat", 88 | }, 89 | }; 90 | const res = { 91 | setHeader: jest.fn(), 92 | send: jest.fn(), 93 | }; 94 | mock 95 | .onPost("https://api.github.com/graphql") 96 | .reply(200, { data: { user: { repository: null }, organization: null } }); 97 | 98 | await pin(req, res); 99 | 100 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml"); 101 | expect(res.send).toBeCalledWith(renderError("User Repository Not found")); 102 | }); 103 | 104 | it("should render error card if org repo not found", async () => { 105 | const req = { 106 | query: { 107 | username: "SrGobi", 108 | repo: "convoychat", 109 | }, 110 | }; 111 | const res = { 112 | setHeader: jest.fn(), 113 | send: jest.fn(), 114 | }; 115 | mock 116 | .onPost("https://api.github.com/graphql") 117 | .reply(200, { data: { user: null, organization: { repository: null } } }); 118 | 119 | await pin(req, res); 120 | 121 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml"); 122 | expect(res.send).toBeCalledWith( 123 | renderError("Organization Repository Not found"), 124 | ); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /tests/renderRepoCard.test.js: -------------------------------------------------------------------------------- 1 | require("@testing-library/jest-dom"); 2 | const cssToObject = require("css-to-object"); 3 | const renderRepoCard = require("../src/cards/repo-card"); 4 | 5 | const { queryByTestId } = require("@testing-library/dom"); 6 | const themes = require("../themes"); 7 | 8 | const data_repo = { 9 | repository: { 10 | nameWithOwner: "SrGobi/convoychat", 11 | name: "convoychat", 12 | stargazers: { totalCount: 38000 }, 13 | description: "Help us take over the world! React + TS + GraphQL Chat App", 14 | primaryLanguage: { 15 | color: "#2b7489", 16 | id: "MDg6TGFuZ3VhZ2UyODc=", 17 | name: "TypeScript", 18 | }, 19 | forkCount: 100, 20 | }, 21 | }; 22 | 23 | describe("Test renderRepoCard", () => { 24 | it("should render correctly", () => { 25 | document.body.innerHTML = renderRepoCard(data_repo.repository); 26 | 27 | const [header] = document.getElementsByClassName("header"); 28 | 29 | expect(header).toHaveTextContent("convoychat"); 30 | expect(header).not.toHaveTextContent("SrGobi"); 31 | expect(document.getElementsByClassName("description")[0]).toHaveTextContent( 32 | "Help us take over the world! React + TS + GraphQL Chat App", 33 | ); 34 | expect(queryByTestId(document.body, "stargazers")).toHaveTextContent("38k"); 35 | expect(queryByTestId(document.body, "forkcount")).toHaveTextContent("100"); 36 | expect(queryByTestId(document.body, "lang-name")).toHaveTextContent( 37 | "TypeScript", 38 | ); 39 | expect(queryByTestId(document.body, "lang-color")).toHaveAttribute( 40 | "fill", 41 | "#2b7489", 42 | ); 43 | }); 44 | 45 | it("should display username in title (full repo name)", () => { 46 | document.body.innerHTML = renderRepoCard(data_repo.repository, { 47 | show_owner: true, 48 | }); 49 | expect(document.getElementsByClassName("header")[0]).toHaveTextContent( 50 | "SrGobi/convoychat", 51 | ); 52 | }); 53 | 54 | it("should trim description", () => { 55 | document.body.innerHTML = renderRepoCard({ 56 | ...data_repo.repository, 57 | description: 58 | "The quick brown fox jumps over the lazy dog is an English-language pangram—a sentence that contains all of the letters of the English alphabet", 59 | }); 60 | 61 | expect( 62 | document.getElementsByClassName("description")[0].children[0].textContent, 63 | ).toBe("The quick brown fox jumps over the lazy dog is an"); 64 | 65 | expect( 66 | document.getElementsByClassName("description")[0].children[1].textContent, 67 | ).toBe("English-language pangram—a sentence that contains all"); 68 | 69 | // Should not trim 70 | document.body.innerHTML = renderRepoCard({ 71 | ...data_repo.repository, 72 | description: "Small text should not trim", 73 | }); 74 | 75 | expect(document.getElementsByClassName("description")[0]).toHaveTextContent( 76 | "Small text should not trim", 77 | ); 78 | }); 79 | 80 | it("should render emojis", () => { 81 | document.body.innerHTML = renderRepoCard({ 82 | ...data_repo.repository, 83 | description: "This is a text with a :poop: poo emoji", 84 | }); 85 | 86 | // poop emoji may not show in all editors but it's there between "a" and "poo" 87 | expect(document.getElementsByClassName("description")[0]).toHaveTextContent( 88 | "This is a text with a 💩 poo emoji", 89 | ); 90 | }); 91 | 92 | it("should shift the text position depending on language length", () => { 93 | document.body.innerHTML = renderRepoCard({ 94 | ...data_repo.repository, 95 | primaryLanguage: { 96 | ...data_repo.repository.primaryLanguage, 97 | name: "Jupyter Notebook", 98 | }, 99 | }); 100 | 101 | expect(queryByTestId(document.body, "primary-lang")).toBeInTheDocument(); 102 | expect(queryByTestId(document.body, "star-fork-group")).toHaveAttribute( 103 | "transform", 104 | "translate(155, 0)", 105 | ); 106 | 107 | // Small lang 108 | document.body.innerHTML = renderRepoCard({ 109 | ...data_repo.repository, 110 | primaryLanguage: { 111 | ...data_repo.repository.primaryLanguage, 112 | name: "Ruby", 113 | }, 114 | }); 115 | 116 | expect(queryByTestId(document.body, "star-fork-group")).toHaveAttribute( 117 | "transform", 118 | "translate(125, 0)", 119 | ); 120 | }); 121 | 122 | it("should hide language if primaryLanguage is null & fallback to correct values", () => { 123 | document.body.innerHTML = renderRepoCard({ 124 | ...data_repo.repository, 125 | primaryLanguage: null, 126 | }); 127 | 128 | expect(queryByTestId(document.body, "primary-lang")).toBeNull(); 129 | 130 | document.body.innerHTML = renderRepoCard({ 131 | ...data_repo.repository, 132 | primaryLanguage: { color: null, name: null }, 133 | }); 134 | 135 | expect(queryByTestId(document.body, "primary-lang")).toBeInTheDocument(); 136 | expect(queryByTestId(document.body, "lang-color")).toHaveAttribute( 137 | "fill", 138 | "#333", 139 | ); 140 | 141 | expect(queryByTestId(document.body, "lang-name")).toHaveTextContent( 142 | "Unspecified", 143 | ); 144 | }); 145 | 146 | it("should render default colors properly", () => { 147 | document.body.innerHTML = renderRepoCard(data_repo.repository); 148 | 149 | const styleTag = document.querySelector("style"); 150 | const stylesObject = cssToObject(styleTag.innerHTML); 151 | 152 | const headerClassStyles = stylesObject[".header"]; 153 | const descClassStyles = stylesObject[".description"]; 154 | const iconClassStyles = stylesObject[".icon"]; 155 | 156 | expect(headerClassStyles.fill).toBe("#2f80ed"); 157 | expect(descClassStyles.fill).toBe("#333"); 158 | expect(iconClassStyles.fill).toBe("#586069"); 159 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute( 160 | "fill", 161 | "#fffefe", 162 | ); 163 | }); 164 | 165 | it("should render custom colors properly", () => { 166 | const customColors = { 167 | title_color: "5a0", 168 | icon_color: "1b998b", 169 | text_color: "9991", 170 | bg_color: "252525", 171 | }; 172 | 173 | document.body.innerHTML = renderRepoCard(data_repo.repository, { 174 | ...customColors, 175 | }); 176 | 177 | const styleTag = document.querySelector("style"); 178 | const stylesObject = cssToObject(styleTag.innerHTML); 179 | 180 | const headerClassStyles = stylesObject[".header"]; 181 | const descClassStyles = stylesObject[".description"]; 182 | const iconClassStyles = stylesObject[".icon"]; 183 | 184 | expect(headerClassStyles.fill).toBe(`#${customColors.title_color}`); 185 | expect(descClassStyles.fill).toBe(`#${customColors.text_color}`); 186 | expect(iconClassStyles.fill).toBe(`#${customColors.icon_color}`); 187 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute( 188 | "fill", 189 | "#252525", 190 | ); 191 | }); 192 | 193 | it("should render with all the themes", () => { 194 | Object.keys(themes).forEach((name) => { 195 | document.body.innerHTML = renderRepoCard(data_repo.repository, { 196 | theme: name, 197 | }); 198 | 199 | const styleTag = document.querySelector("style"); 200 | const stylesObject = cssToObject(styleTag.innerHTML); 201 | 202 | const headerClassStyles = stylesObject[".header"]; 203 | const descClassStyles = stylesObject[".description"]; 204 | const iconClassStyles = stylesObject[".icon"]; 205 | 206 | expect(headerClassStyles.fill).toBe(`#${themes[name].title_color}`); 207 | expect(descClassStyles.fill).toBe(`#${themes[name].text_color}`); 208 | expect(iconClassStyles.fill).toBe(`#${themes[name].icon_color}`); 209 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute( 210 | "fill", 211 | `#${themes[name].bg_color}`, 212 | ); 213 | }); 214 | }); 215 | 216 | it("should render custom colors with themes", () => { 217 | document.body.innerHTML = renderRepoCard(data_repo.repository, { 218 | title_color: "5a0", 219 | theme: "radical", 220 | }); 221 | 222 | const styleTag = document.querySelector("style"); 223 | const stylesObject = cssToObject(styleTag.innerHTML); 224 | 225 | const headerClassStyles = stylesObject[".header"]; 226 | const descClassStyles = stylesObject[".description"]; 227 | const iconClassStyles = stylesObject[".icon"]; 228 | 229 | expect(headerClassStyles.fill).toBe("#5a0"); 230 | expect(descClassStyles.fill).toBe(`#${themes.radical.text_color}`); 231 | expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`); 232 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute( 233 | "fill", 234 | `#${themes.radical.bg_color}`, 235 | ); 236 | }); 237 | 238 | it("should render custom colors with themes and fallback to default colors if invalid", () => { 239 | document.body.innerHTML = renderRepoCard(data_repo.repository, { 240 | title_color: "invalid color", 241 | text_color: "invalid color", 242 | theme: "radical", 243 | }); 244 | 245 | const styleTag = document.querySelector("style"); 246 | const stylesObject = cssToObject(styleTag.innerHTML); 247 | 248 | const headerClassStyles = stylesObject[".header"]; 249 | const descClassStyles = stylesObject[".description"]; 250 | const iconClassStyles = stylesObject[".icon"]; 251 | 252 | expect(headerClassStyles.fill).toBe(`#${themes.default.title_color}`); 253 | expect(descClassStyles.fill).toBe(`#${themes.default.text_color}`); 254 | expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`); 255 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute( 256 | "fill", 257 | `#${themes.radical.bg_color}`, 258 | ); 259 | }); 260 | 261 | it("should not render star count or fork count if either of the are zero", () => { 262 | document.body.innerHTML = renderRepoCard({ 263 | ...data_repo.repository, 264 | stargazers: { totalCount: 0 }, 265 | }); 266 | 267 | expect(queryByTestId(document.body, "stargazers")).toBeNull(); 268 | expect(queryByTestId(document.body, "forkcount")).toBeInTheDocument(); 269 | 270 | document.body.innerHTML = renderRepoCard({ 271 | ...data_repo.repository, 272 | stargazers: { totalCount: 1 }, 273 | forkCount: 0, 274 | }); 275 | 276 | expect(queryByTestId(document.body, "stargazers")).toBeInTheDocument(); 277 | expect(queryByTestId(document.body, "forkcount")).toBeNull(); 278 | 279 | document.body.innerHTML = renderRepoCard({ 280 | ...data_repo.repository, 281 | stargazers: { totalCount: 0 }, 282 | forkCount: 0, 283 | }); 284 | 285 | expect(queryByTestId(document.body, "stargazers")).toBeNull(); 286 | expect(queryByTestId(document.body, "forkcount")).toBeNull(); 287 | }); 288 | 289 | it("should render badges", () => { 290 | document.body.innerHTML = renderRepoCard({ 291 | ...data_repo.repository, 292 | isArchived: true, 293 | }); 294 | 295 | expect(queryByTestId(document.body, "badge")).toHaveTextContent("Archived"); 296 | 297 | document.body.innerHTML = renderRepoCard({ 298 | ...data_repo.repository, 299 | isTemplate: true, 300 | }); 301 | expect(queryByTestId(document.body, "badge")).toHaveTextContent("Template"); 302 | }); 303 | 304 | it("should not render template", () => { 305 | document.body.innerHTML = renderRepoCard({ 306 | ...data_repo.repository, 307 | }); 308 | expect(queryByTestId(document.body, "badge")).toBeNull(); 309 | }); 310 | 311 | it("should render translated badges", () => { 312 | document.body.innerHTML = renderRepoCard( 313 | { 314 | ...data_repo.repository, 315 | isArchived: true, 316 | }, 317 | { 318 | locale: "cn", 319 | }, 320 | ); 321 | 322 | expect(queryByTestId(document.body, "badge")).toHaveTextContent("已归档"); 323 | 324 | document.body.innerHTML = renderRepoCard( 325 | { 326 | ...data_repo.repository, 327 | isTemplate: true, 328 | }, 329 | { 330 | locale: "cn", 331 | }, 332 | ); 333 | expect(queryByTestId(document.body, "badge")).toHaveTextContent("模板"); 334 | }); 335 | }); 336 | -------------------------------------------------------------------------------- /tests/renderStatsCard.test.js: -------------------------------------------------------------------------------- 1 | require("@testing-library/jest-dom"); 2 | const cssToObject = require("css-to-object"); 3 | const renderStatsCard = require("../src/cards/stats-card"); 4 | 5 | const { 6 | getByTestId, 7 | queryByTestId, 8 | queryAllByTestId, 9 | } = require("@testing-library/dom"); 10 | const themes = require("../themes"); 11 | 12 | describe("Test renderStatsCard", () => { 13 | const stats = { 14 | name: "Anurag Hazra", 15 | totalStars: 100, 16 | totalCommits: 200, 17 | totalIssues: 300, 18 | totalPRs: 400, 19 | contributedTo: 500, 20 | rank: { level: "A+", score: 40 }, 21 | }; 22 | 23 | it("should render correctly", () => { 24 | document.body.innerHTML = renderStatsCard(stats); 25 | 26 | expect(document.getElementsByClassName("header")[0].textContent).toBe( 27 | "Anurag Hazra's GitHub Stats", 28 | ); 29 | 30 | expect( 31 | document.body.getElementsByTagName("svg")[0].getAttribute("height"), 32 | ).toBe("195"); 33 | expect(getByTestId(document.body, "stars").textContent).toBe("100"); 34 | expect(getByTestId(document.body, "commits").textContent).toBe("200"); 35 | expect(getByTestId(document.body, "issues").textContent).toBe("300"); 36 | expect(getByTestId(document.body, "prs").textContent).toBe("400"); 37 | expect(getByTestId(document.body, "contribs").textContent).toBe("500"); 38 | expect(queryByTestId(document.body, "card-bg")).toBeInTheDocument(); 39 | expect(queryByTestId(document.body, "rank-circle")).toBeInTheDocument(); 40 | }); 41 | 42 | it("should have proper name apostrophe", () => { 43 | document.body.innerHTML = renderStatsCard({ ...stats, name: "Anil Das" }); 44 | 45 | expect(document.getElementsByClassName("header")[0].textContent).toBe( 46 | "Anil Das' GitHub Stats", 47 | ); 48 | 49 | document.body.innerHTML = renderStatsCard({ ...stats, name: "Felix" }); 50 | 51 | expect(document.getElementsByClassName("header")[0].textContent).toBe( 52 | "Felix' GitHub Stats", 53 | ); 54 | }); 55 | 56 | it("should hide individual stats", () => { 57 | document.body.innerHTML = renderStatsCard(stats, { 58 | hide: ["issues", "prs", "contribs"], 59 | }); 60 | 61 | expect( 62 | document.body.getElementsByTagName("svg")[0].getAttribute("height"), 63 | ).toBe("150"); // height should be 150 because we clamped it. 64 | 65 | expect(queryByTestId(document.body, "stars")).toBeDefined(); 66 | expect(queryByTestId(document.body, "commits")).toBeDefined(); 67 | expect(queryByTestId(document.body, "issues")).toBeNull(); 68 | expect(queryByTestId(document.body, "prs")).toBeNull(); 69 | expect(queryByTestId(document.body, "contribs")).toBeNull(); 70 | }); 71 | 72 | it("should hide_rank", () => { 73 | document.body.innerHTML = renderStatsCard(stats, { hide_rank: true }); 74 | 75 | expect(queryByTestId(document.body, "rank-circle")).not.toBeInTheDocument(); 76 | }); 77 | 78 | it("should render default colors properly", () => { 79 | document.body.innerHTML = renderStatsCard(stats); 80 | 81 | const styleTag = document.querySelector("style"); 82 | const stylesObject = cssToObject(styleTag.textContent); 83 | 84 | const headerClassStyles = stylesObject[".header"]; 85 | const statClassStyles = stylesObject[".stat"]; 86 | const iconClassStyles = stylesObject[".icon"]; 87 | 88 | expect(headerClassStyles.fill).toBe("#2f80ed"); 89 | expect(statClassStyles.fill).toBe("#333"); 90 | expect(iconClassStyles.fill).toBe("#4c71f2"); 91 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute( 92 | "fill", 93 | "#fffefe", 94 | ); 95 | }); 96 | 97 | it("should render custom colors properly", () => { 98 | const customColors = { 99 | title_color: "5a0", 100 | icon_color: "1b998b", 101 | text_color: "9991", 102 | bg_color: "252525", 103 | }; 104 | 105 | document.body.innerHTML = renderStatsCard(stats, { ...customColors }); 106 | 107 | const styleTag = document.querySelector("style"); 108 | const stylesObject = cssToObject(styleTag.innerHTML); 109 | 110 | const headerClassStyles = stylesObject[".header"]; 111 | const statClassStyles = stylesObject[".stat"]; 112 | const iconClassStyles = stylesObject[".icon"]; 113 | 114 | expect(headerClassStyles.fill).toBe(`#${customColors.title_color}`); 115 | expect(statClassStyles.fill).toBe(`#${customColors.text_color}`); 116 | expect(iconClassStyles.fill).toBe(`#${customColors.icon_color}`); 117 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute( 118 | "fill", 119 | "#252525", 120 | ); 121 | }); 122 | 123 | it("should render custom colors with themes", () => { 124 | document.body.innerHTML = renderStatsCard(stats, { 125 | title_color: "5a0", 126 | theme: "radical", 127 | }); 128 | 129 | const styleTag = document.querySelector("style"); 130 | const stylesObject = cssToObject(styleTag.innerHTML); 131 | 132 | const headerClassStyles = stylesObject[".header"]; 133 | const statClassStyles = stylesObject[".stat"]; 134 | const iconClassStyles = stylesObject[".icon"]; 135 | 136 | expect(headerClassStyles.fill).toBe("#5a0"); 137 | expect(statClassStyles.fill).toBe(`#${themes.radical.text_color}`); 138 | expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`); 139 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute( 140 | "fill", 141 | `#${themes.radical.bg_color}`, 142 | ); 143 | }); 144 | 145 | it("should render with all the themes", () => { 146 | Object.keys(themes).forEach((name) => { 147 | document.body.innerHTML = renderStatsCard(stats, { 148 | theme: name, 149 | }); 150 | 151 | const styleTag = document.querySelector("style"); 152 | const stylesObject = cssToObject(styleTag.innerHTML); 153 | 154 | const headerClassStyles = stylesObject[".header"]; 155 | const statClassStyles = stylesObject[".stat"]; 156 | const iconClassStyles = stylesObject[".icon"]; 157 | 158 | expect(headerClassStyles.fill).toBe(`#${themes[name].title_color}`); 159 | expect(statClassStyles.fill).toBe(`#${themes[name].text_color}`); 160 | expect(iconClassStyles.fill).toBe(`#${themes[name].icon_color}`); 161 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute( 162 | "fill", 163 | `#${themes[name].bg_color}`, 164 | ); 165 | }); 166 | }); 167 | 168 | it("should render custom colors with themes and fallback to default colors if invalid", () => { 169 | document.body.innerHTML = renderStatsCard(stats, { 170 | title_color: "invalid color", 171 | text_color: "invalid color", 172 | theme: "radical", 173 | }); 174 | 175 | const styleTag = document.querySelector("style"); 176 | const stylesObject = cssToObject(styleTag.innerHTML); 177 | 178 | const headerClassStyles = stylesObject[".header"]; 179 | const statClassStyles = stylesObject[".stat"]; 180 | const iconClassStyles = stylesObject[".icon"]; 181 | 182 | expect(headerClassStyles.fill).toBe(`#${themes.default.title_color}`); 183 | expect(statClassStyles.fill).toBe(`#${themes.default.text_color}`); 184 | expect(iconClassStyles.fill).toBe(`#${themes.radical.icon_color}`); 185 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute( 186 | "fill", 187 | `#${themes.radical.bg_color}`, 188 | ); 189 | }); 190 | 191 | it("should render icons correctly", () => { 192 | document.body.innerHTML = renderStatsCard(stats, { 193 | show_icons: true, 194 | }); 195 | 196 | expect(queryAllByTestId(document.body, "icon")[0]).toBeDefined(); 197 | expect(queryByTestId(document.body, "stars")).toBeDefined(); 198 | expect( 199 | queryByTestId(document.body, "stars").previousElementSibling, // the label 200 | ).toHaveAttribute("x", "25"); 201 | }); 202 | 203 | it("should not have icons if show_icons is false", () => { 204 | document.body.innerHTML = renderStatsCard(stats, { show_icons: false }); 205 | 206 | expect(queryAllByTestId(document.body, "icon")[0]).not.toBeDefined(); 207 | expect(queryByTestId(document.body, "stars")).toBeDefined(); 208 | expect( 209 | queryByTestId(document.body, "stars").previousElementSibling, // the label 210 | ).not.toHaveAttribute("x"); 211 | }); 212 | 213 | it("should auto resize if hide_rank is true", () => { 214 | document.body.innerHTML = renderStatsCard(stats, { 215 | hide_rank: true, 216 | }); 217 | 218 | expect( 219 | document.body.getElementsByTagName("svg")[0].getAttribute("width"), 220 | ).toBe("305.81250000000006"); 221 | }); 222 | 223 | it("should auto resize if hide_rank is true & custom_title is set", () => { 224 | document.body.innerHTML = renderStatsCard(stats, { 225 | hide_rank: true, 226 | custom_title: "Hello world", 227 | }); 228 | 229 | expect( 230 | document.body.getElementsByTagName("svg")[0].getAttribute("width"), 231 | ).toBe("270"); 232 | }); 233 | 234 | it("should render translations", () => { 235 | document.body.innerHTML = renderStatsCard(stats, { locale: "cn" }); 236 | expect(document.getElementsByClassName("header")[0].textContent).toBe( 237 | "Anurag Hazra 的 GitHub 统计", 238 | ); 239 | expect( 240 | document.querySelector( 241 | 'g[transform="translate(0, 0)"]>.stagger>.stat.bold', 242 | ).textContent, 243 | ).toBe("获标星(star):"); 244 | expect( 245 | document.querySelector( 246 | 'g[transform="translate(0, 25)"]>.stagger>.stat.bold', 247 | ).textContent, 248 | ).toBe(`累计提交(commit) (${new Date().getFullYear()}):`); 249 | expect( 250 | document.querySelector( 251 | 'g[transform="translate(0, 50)"]>.stagger>.stat.bold', 252 | ).textContent, 253 | ).toBe("提案数(PR):"); 254 | expect( 255 | document.querySelector( 256 | 'g[transform="translate(0, 75)"]>.stagger>.stat.bold', 257 | ).textContent, 258 | ).toBe("指出问题(issue):"); 259 | expect( 260 | document.querySelector( 261 | 'g[transform="translate(0, 100)"]>.stagger>.stat.bold', 262 | ).textContent, 263 | ).toBe("参与项目数:"); 264 | }); 265 | }); 266 | -------------------------------------------------------------------------------- /tests/renderTopLanguages.test.js: -------------------------------------------------------------------------------- 1 | require("@testing-library/jest-dom"); 2 | const cssToObject = require("css-to-object"); 3 | const renderTopLanguages = require("../src/cards/top-languages-card"); 4 | 5 | const { queryByTestId, queryAllByTestId } = require("@testing-library/dom"); 6 | const themes = require("../themes"); 7 | 8 | describe("Test renderTopLanguages", () => { 9 | const langs = { 10 | HTML: { 11 | color: "#0f0", 12 | name: "HTML", 13 | size: 200, 14 | }, 15 | javascript: { 16 | color: "#0ff", 17 | name: "javascript", 18 | size: 200, 19 | }, 20 | css: { 21 | color: "#ff0", 22 | name: "css", 23 | size: 100, 24 | }, 25 | }; 26 | 27 | it("should render correctly", () => { 28 | document.body.innerHTML = renderTopLanguages(langs); 29 | 30 | expect(queryByTestId(document.body, "header")).toHaveTextContent( 31 | "Most Used Languages", 32 | ); 33 | 34 | expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent( 35 | "HTML", 36 | ); 37 | expect(queryAllByTestId(document.body, "lang-name")[1]).toHaveTextContent( 38 | "javascript", 39 | ); 40 | expect(queryAllByTestId(document.body, "lang-name")[2]).toHaveTextContent( 41 | "css", 42 | ); 43 | expect(queryAllByTestId(document.body, "lang-progress")[0]).toHaveAttribute( 44 | "width", 45 | "40%", 46 | ); 47 | expect(queryAllByTestId(document.body, "lang-progress")[1]).toHaveAttribute( 48 | "width", 49 | "40%", 50 | ); 51 | expect(queryAllByTestId(document.body, "lang-progress")[2]).toHaveAttribute( 52 | "width", 53 | "20%", 54 | ); 55 | }); 56 | 57 | it("should hide languages when hide is passed", () => { 58 | document.body.innerHTML = renderTopLanguages(langs, { 59 | hide: ["HTML"], 60 | }); 61 | expect(queryAllByTestId(document.body, "lang-name")[0]).toBeInTheDocument( 62 | "javascript", 63 | ); 64 | expect(queryAllByTestId(document.body, "lang-name")[1]).toBeInTheDocument( 65 | "css", 66 | ); 67 | expect(queryAllByTestId(document.body, "lang-name")[2]).not.toBeDefined(); 68 | 69 | // multiple languages passed 70 | document.body.innerHTML = renderTopLanguages(langs, { 71 | hide: ["HTML", "css"], 72 | }); 73 | expect(queryAllByTestId(document.body, "lang-name")[0]).toBeInTheDocument( 74 | "javascript", 75 | ); 76 | expect(queryAllByTestId(document.body, "lang-name")[1]).not.toBeDefined(); 77 | }); 78 | 79 | it("should resize the height correctly depending on langs", () => { 80 | document.body.innerHTML = renderTopLanguages(langs, {}); 81 | expect(document.querySelector("svg")).toHaveAttribute("height", "205"); 82 | 83 | document.body.innerHTML = renderTopLanguages( 84 | { 85 | ...langs, 86 | python: { 87 | color: "#ff0", 88 | name: "python", 89 | size: 100, 90 | }, 91 | }, 92 | {}, 93 | ); 94 | expect(document.querySelector("svg")).toHaveAttribute("height", "245"); 95 | }); 96 | 97 | it("should render with custom width set", () => { 98 | document.body.innerHTML = renderTopLanguages(langs, {}); 99 | 100 | expect(document.querySelector("svg")).toHaveAttribute("width", "300"); 101 | 102 | document.body.innerHTML = renderTopLanguages(langs, { card_width: 400 }); 103 | expect(document.querySelector("svg")).toHaveAttribute("width", "400"); 104 | }); 105 | 106 | it("should render default colors properly", () => { 107 | document.body.innerHTML = renderTopLanguages(langs); 108 | 109 | const styleTag = document.querySelector("style"); 110 | const stylesObject = cssToObject(styleTag.textContent); 111 | 112 | const headerStyles = stylesObject[".header"]; 113 | const langNameStyles = stylesObject[".lang-name"]; 114 | 115 | expect(headerStyles.fill).toBe("#2f80ed"); 116 | expect(langNameStyles.fill).toBe("#333"); 117 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute( 118 | "fill", 119 | "#fffefe", 120 | ); 121 | }); 122 | 123 | it("should render custom colors properly", () => { 124 | const customColors = { 125 | title_color: "5a0", 126 | icon_color: "1b998b", 127 | text_color: "9991", 128 | bg_color: "252525", 129 | }; 130 | 131 | document.body.innerHTML = renderTopLanguages(langs, { ...customColors }); 132 | 133 | const styleTag = document.querySelector("style"); 134 | const stylesObject = cssToObject(styleTag.innerHTML); 135 | 136 | const headerStyles = stylesObject[".header"]; 137 | const langNameStyles = stylesObject[".lang-name"]; 138 | 139 | expect(headerStyles.fill).toBe(`#${customColors.title_color}`); 140 | expect(langNameStyles.fill).toBe(`#${customColors.text_color}`); 141 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute( 142 | "fill", 143 | "#252525", 144 | ); 145 | }); 146 | 147 | it("should render custom colors with themes", () => { 148 | document.body.innerHTML = renderTopLanguages(langs, { 149 | title_color: "5a0", 150 | theme: "radical", 151 | }); 152 | 153 | const styleTag = document.querySelector("style"); 154 | const stylesObject = cssToObject(styleTag.innerHTML); 155 | 156 | const headerStyles = stylesObject[".header"]; 157 | const langNameStyles = stylesObject[".lang-name"]; 158 | 159 | expect(headerStyles.fill).toBe("#5a0"); 160 | expect(langNameStyles.fill).toBe(`#${themes.radical.text_color}`); 161 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute( 162 | "fill", 163 | `#${themes.radical.bg_color}`, 164 | ); 165 | }); 166 | 167 | it("should render with all the themes", () => { 168 | Object.keys(themes).forEach((name) => { 169 | document.body.innerHTML = renderTopLanguages(langs, { 170 | theme: name, 171 | }); 172 | 173 | const styleTag = document.querySelector("style"); 174 | const stylesObject = cssToObject(styleTag.innerHTML); 175 | 176 | const headerStyles = stylesObject[".header"]; 177 | const langNameStyles = stylesObject[".lang-name"]; 178 | 179 | expect(headerStyles.fill).toBe(`#${themes[name].title_color}`); 180 | expect(langNameStyles.fill).toBe(`#${themes[name].text_color}`); 181 | expect(queryByTestId(document.body, "card-bg")).toHaveAttribute( 182 | "fill", 183 | `#${themes[name].bg_color}`, 184 | ); 185 | }); 186 | }); 187 | 188 | it("should render with layout compact", () => { 189 | document.body.innerHTML = renderTopLanguages(langs, { layout: "compact" }); 190 | 191 | expect(queryByTestId(document.body, "header")).toHaveTextContent( 192 | "Most Used Languages", 193 | ); 194 | 195 | expect(queryAllByTestId(document.body, "lang-name")[0]).toHaveTextContent( 196 | "HTML 40.00%", 197 | ); 198 | expect(queryAllByTestId(document.body, "lang-progress")[0]).toHaveAttribute( 199 | "width", 200 | "120.00", 201 | ); 202 | 203 | expect(queryAllByTestId(document.body, "lang-name")[1]).toHaveTextContent( 204 | "javascript 40.00%", 205 | ); 206 | expect(queryAllByTestId(document.body, "lang-progress")[1]).toHaveAttribute( 207 | "width", 208 | "120.00", 209 | ); 210 | 211 | expect(queryAllByTestId(document.body, "lang-name")[2]).toHaveTextContent( 212 | "css 20.00%", 213 | ); 214 | expect(queryAllByTestId(document.body, "lang-progress")[2]).toHaveAttribute( 215 | "width", 216 | "60.00", 217 | ); 218 | }); 219 | 220 | it("should render a translated title", () => { 221 | document.body.innerHTML = renderTopLanguages(langs, { locale: "cn" }); 222 | expect(document.getElementsByClassName("header")[0].textContent).toBe( 223 | "最常用的语言", 224 | ); 225 | }); 226 | }); 227 | -------------------------------------------------------------------------------- /tests/renderWakatimeCard.test.js: -------------------------------------------------------------------------------- 1 | require("@testing-library/jest-dom"); 2 | const renderWakatimeCard = require("../src/cards/wakatime-card"); 3 | 4 | const { wakaTimeData } = require("./fetchWakatime.test"); 5 | 6 | describe("Test Render Wakatime Card", () => { 7 | it("should render correctly", () => { 8 | const card = renderWakatimeCard(wakaTimeData.data); 9 | 10 | expect(card).toMatchSnapshot(); 11 | }); 12 | 13 | it("should render correctly with compact layout", () => { 14 | const card = renderWakatimeCard(wakaTimeData.data, { layout: "compact" }); 15 | 16 | expect(card).toMatchSnapshot(); 17 | }); 18 | 19 | it("should render translations", () => { 20 | document.body.innerHTML = renderWakatimeCard({}, { locale: "cn" }); 21 | expect(document.getElementsByClassName("header")[0].textContent).toBe( 22 | "Wakatime 周统计", 23 | ); 24 | expect( 25 | document.querySelector('g[transform="translate(0, 0)"]>text.stat.bold') 26 | .textContent, 27 | ).toBe("本周没有编程活动"); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/retryer.test.js: -------------------------------------------------------------------------------- 1 | require("@testing-library/jest-dom"); 2 | const retryer = require("../src/common/retryer"); 3 | const { logger } = require("../src/common/utils"); 4 | 5 | const fetcher = jest.fn((variables, token) => { 6 | logger.log(variables, token); 7 | return new Promise((res, rej) => res({ data: "ok" })); 8 | }); 9 | 10 | const fetcherFail = jest.fn(() => { 11 | return new Promise((res, rej) => 12 | res({ data: { errors: [{ type: "RATE_LIMITED" }] } }), 13 | ); 14 | }); 15 | 16 | const fetcherFailOnSecondTry = jest.fn((_vars, _token, retries) => { 17 | return new Promise((res, rej) => { 18 | // faking rate limit 19 | if (retries < 1) { 20 | return res({ data: { errors: [{ type: "RATE_LIMITED" }] } }); 21 | } 22 | return res({ data: "ok" }); 23 | }); 24 | }); 25 | 26 | describe("Test Retryer", () => { 27 | it("retryer should return value and have zero retries on first try", async () => { 28 | let res = await retryer(fetcher, {}); 29 | 30 | expect(fetcher).toBeCalledTimes(1); 31 | expect(res).toStrictEqual({ data: "ok" }); 32 | }); 33 | 34 | it("retryer should return value and have 2 retries", async () => { 35 | let res = await retryer(fetcherFailOnSecondTry, {}); 36 | 37 | expect(fetcherFailOnSecondTry).toBeCalledTimes(2); 38 | expect(res).toStrictEqual({ data: "ok" }); 39 | }); 40 | 41 | it("retryer should throw error if maximum retries reached", async () => { 42 | let res; 43 | 44 | try { 45 | res = await retryer(fetcherFail, {}); 46 | } catch (err) { 47 | expect(fetcherFail).toBeCalledTimes(8); 48 | expect(err.message).toBe("Maximum retries exceeded"); 49 | } 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/top-langs.test.js: -------------------------------------------------------------------------------- 1 | require("@testing-library/jest-dom"); 2 | const axios = require("axios"); 3 | const MockAdapter = require("axios-mock-adapter"); 4 | const topLangs = require("../api/top-langs"); 5 | const renderTopLanguages = require("../src/cards/top-languages-card"); 6 | const { renderError } = require("../src/common/utils"); 7 | 8 | const data_langs = { 9 | data: { 10 | user: { 11 | repositories: { 12 | nodes: [ 13 | { 14 | languages: { 15 | edges: [{ size: 150, node: { color: "#0f0", name: "HTML" } }], 16 | }, 17 | }, 18 | { 19 | languages: { 20 | edges: [{ size: 100, node: { color: "#0f0", name: "HTML" } }], 21 | }, 22 | }, 23 | { 24 | languages: { 25 | edges: [ 26 | { size: 100, node: { color: "#0ff", name: "javascript" } }, 27 | ], 28 | }, 29 | }, 30 | { 31 | languages: { 32 | edges: [ 33 | { size: 100, node: { color: "#0ff", name: "javascript" } }, 34 | ], 35 | }, 36 | }, 37 | ], 38 | }, 39 | }, 40 | }, 41 | }; 42 | 43 | const error = { 44 | errors: [ 45 | { 46 | type: "NOT_FOUND", 47 | path: ["user"], 48 | locations: [], 49 | message: "Could not fetch user", 50 | }, 51 | ], 52 | }; 53 | 54 | const langs = { 55 | HTML: { 56 | color: "#0f0", 57 | name: "HTML", 58 | size: 250, 59 | }, 60 | javascript: { 61 | color: "#0ff", 62 | name: "javascript", 63 | size: 200, 64 | }, 65 | }; 66 | 67 | const mock = new MockAdapter(axios); 68 | 69 | afterEach(() => { 70 | mock.reset(); 71 | }); 72 | 73 | describe("Test /api/top-langs", () => { 74 | it("should test the request", async () => { 75 | const req = { 76 | query: { 77 | username: "SrGobi", 78 | }, 79 | }; 80 | const res = { 81 | setHeader: jest.fn(), 82 | send: jest.fn(), 83 | }; 84 | mock.onPost("https://api.github.com/graphql").reply(200, data_langs); 85 | 86 | await topLangs(req, res); 87 | 88 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml"); 89 | expect(res.send).toBeCalledWith(renderTopLanguages(langs)); 90 | }); 91 | 92 | it("should work with the query options", async () => { 93 | const req = { 94 | query: { 95 | username: "SrGobi", 96 | hide_title: true, 97 | card_width: 100, 98 | title_color: "fff", 99 | icon_color: "fff", 100 | text_color: "fff", 101 | bg_color: "fff", 102 | }, 103 | }; 104 | const res = { 105 | setHeader: jest.fn(), 106 | send: jest.fn(), 107 | }; 108 | mock.onPost("https://api.github.com/graphql").reply(200, data_langs); 109 | 110 | await topLangs(req, res); 111 | 112 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml"); 113 | expect(res.send).toBeCalledWith( 114 | renderTopLanguages(langs, { 115 | hide_title: true, 116 | card_width: 100, 117 | title_color: "fff", 118 | icon_color: "fff", 119 | text_color: "fff", 120 | bg_color: "fff", 121 | }), 122 | ); 123 | }); 124 | 125 | it("should render error card on error", async () => { 126 | const req = { 127 | query: { 128 | username: "SrGobi", 129 | }, 130 | }; 131 | const res = { 132 | setHeader: jest.fn(), 133 | send: jest.fn(), 134 | }; 135 | mock.onPost("https://api.github.com/graphql").reply(200, error); 136 | 137 | await topLangs(req, res); 138 | 139 | expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml"); 140 | expect(res.send).toBeCalledWith(renderError(error.errors[0].message)); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /tests/utils.test.js: -------------------------------------------------------------------------------- 1 | require("@testing-library/jest-dom"); 2 | const { 3 | kFormatter, 4 | encodeHTML, 5 | renderError, 6 | FlexLayout, 7 | getCardColors, 8 | wrapTextMultiline, 9 | } = require("../src/common/utils"); 10 | 11 | const { queryByTestId } = require("@testing-library/dom"); 12 | 13 | describe("Test utils.js", () => { 14 | it("should test kFormatter", () => { 15 | expect(kFormatter(1)).toBe(1); 16 | expect(kFormatter(-1)).toBe(-1); 17 | expect(kFormatter(500)).toBe(500); 18 | expect(kFormatter(1000)).toBe("1k"); 19 | expect(kFormatter(10000)).toBe("10k"); 20 | expect(kFormatter(12345)).toBe("12.3k"); 21 | expect(kFormatter(9900000)).toBe("9900k"); 22 | }); 23 | 24 | it("should test encodeHTML", () => { 25 | expect(encodeHTML(`hello world<,.#4^&^@%!))`)).toBe( 26 | "<html>hello world<,.#4^&^@%!))", 27 | ); 28 | }); 29 | 30 | it("should test renderError", () => { 31 | document.body.innerHTML = renderError("Something went wrong"); 32 | expect( 33 | queryByTestId(document.body, "message").children[0], 34 | ).toHaveTextContent(/Something went wrong/gim); 35 | expect(queryByTestId(document.body, "message").children[1]).toBeEmpty(2); 36 | 37 | // Secondary message 38 | document.body.innerHTML = renderError( 39 | "Something went wrong", 40 | "Secondary Message", 41 | ); 42 | expect( 43 | queryByTestId(document.body, "message").children[1], 44 | ).toHaveTextContent(/Secondary Message/gim); 45 | }); 46 | 47 | it("should test FlexLayout", () => { 48 | const layout = FlexLayout({ 49 | items: ["1", "2"], 50 | gap: 60, 51 | }).join(""); 52 | 53 | expect(layout).toBe( 54 | `12`, 55 | ); 56 | 57 | const columns = FlexLayout({ 58 | items: ["1", "2"], 59 | gap: 60, 60 | direction: "column", 61 | }).join(""); 62 | 63 | expect(columns).toBe( 64 | `12`, 65 | ); 66 | }); 67 | 68 | it("getCardColors: should return expected values", () => { 69 | let colors = getCardColors({ 70 | title_color: "f00", 71 | text_color: "0f0", 72 | icon_color: "00f", 73 | bg_color: "fff", 74 | theme: "dark", 75 | }); 76 | expect(colors).toStrictEqual({ 77 | titleColor: "#f00", 78 | textColor: "#0f0", 79 | iconColor: "#00f", 80 | bgColor: "#fff", 81 | }); 82 | }); 83 | 84 | it("getCardColors: should fallback to default colors if color is invalid", () => { 85 | let colors = getCardColors({ 86 | title_color: "invalidcolor", 87 | text_color: "0f0", 88 | icon_color: "00f", 89 | bg_color: "fff", 90 | theme: "dark", 91 | }); 92 | expect(colors).toStrictEqual({ 93 | titleColor: "#2f80ed", 94 | textColor: "#0f0", 95 | iconColor: "#00f", 96 | bgColor: "#fff", 97 | }); 98 | }); 99 | 100 | it("getCardColors: should fallback to specified theme colors if is not defined", () => { 101 | let colors = getCardColors({ 102 | theme: "dark", 103 | }); 104 | expect(colors).toStrictEqual({ 105 | titleColor: "#fff", 106 | textColor: "#9f9f9f", 107 | iconColor: "#79ff97", 108 | bgColor: "#151515", 109 | }); 110 | }); 111 | }); 112 | 113 | describe("wrapTextMultiline", () => { 114 | it("should not wrap small texts", () => { 115 | { 116 | let multiLineText = wrapTextMultiline("Small text should not wrap"); 117 | expect(multiLineText).toEqual(["Small text should not wrap"]); 118 | } 119 | }); 120 | it("should wrap large texts", () => { 121 | let multiLineText = wrapTextMultiline( 122 | "Hello world long long long text", 123 | 20, 124 | 3, 125 | ); 126 | expect(multiLineText).toEqual(["Hello world long", "long long text"]); 127 | }); 128 | it("should wrap large texts and limit max lines", () => { 129 | let multiLineText = wrapTextMultiline( 130 | "Hello world long long long text", 131 | 10, 132 | 2, 133 | ); 134 | expect(multiLineText).toEqual(["Hello", "world long..."]); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /themes/README.md: -------------------------------------------------------------------------------- 1 | ## Available Themes 2 | 3 | 4 | 5 | With inbuilt themes you can customize the look of the card without doing any manual customization. 6 | 7 | Use `?theme=THEME_NAME` parameter like so :- 8 | 9 | ```md 10 | ![Anurag's GitHub stats](https://github-readme-stats.vercel.app/api?username=SrGobi&theme=dark&show_icons=true) 11 | ``` 12 | 13 | ## Stats 14 | 15 | > These themes work both for the Stats Card and Repo Card. 16 | 17 | | | | | 18 | | :--: | :--: | :--: | 19 | | `default` ![default][default] | `dark` ![dark][dark] | `radical` ![radical][radical] | 20 | | `merko` ![merko][merko] | `gruvbox` ![gruvbox][gruvbox] | `tokyonight` ![tokyonight][tokyonight] | 21 | | `onedark` ![onedark][onedark] | `cobalt` ![cobalt][cobalt] | `synthwave` ![synthwave][synthwave] | 22 | | `highcontrast` ![highcontrast][highcontrast] | `dracula` ![dracula][dracula] | `prussian` ![prussian][prussian] | 23 | | `monokai` ![monokai][monokai] | `vue` ![vue][vue] | `vue-dark` ![vue-dark][vue-dark] | 24 | | `shades-of-purple` ![shades-of-purple][shades-of-purple] | `nightowl` ![nightowl][nightowl] | `buefy` ![buefy][buefy] | 25 | | `blue-green` ![blue-green][blue-green] | `algolia` ![algolia][algolia] | `great-gatsby` ![great-gatsby][great-gatsby] | 26 | | `darcula` ![darcula][darcula] | `bear` ![bear][bear] | `solarized-dark` ![solarized-dark][solarized-dark] | 27 | | `solarized-light` ![solarized-light][solarized-light] | `chartreuse-dark` ![chartreuse-dark][chartreuse-dark] | `nord` ![nord][nord] | 28 | | `gotham` ![gotham][gotham] | `material-palenight` ![material-palenight][material-palenight] | `graywhite` ![graywhite][graywhite] | 29 | | `vision-friendly-dark` ![vision-friendly-dark][vision-friendly-dark] | `ayu-mirage` ![ayu-mirage][ayu-mirage] | `midnight-purple` ![midnight-purple][midnight-purple] | 30 | | `calm` ![calm][calm] | `flag-india` ![flag-india][flag-india] | `omni` ![omni][omni] | 31 | | `react` ![react][react] | `jolly` ![jolly][jolly] | `maroongold` ![maroongold][maroongold] | 32 | | `yeblu` ![yeblu][yeblu] | `blueberry` ![blueberry][blueberry] | `slateorange` ![slateorange][slateorange] | 33 | | `kacho_ga` ![kacho_ga][kacho_ga] | `outrun` ![outrun][outrun] | [Add your theme][add-theme] | 34 | 35 | ## Repo Card 36 | 37 | > These themes work both for the Stats Card and Repo Card. 38 | 39 | | | | | 40 | | :--: | :--: | :--: | 41 | | `default_repocard` ![default_repocard][default_repocard_repo] | `dark` ![dark][dark_repo] | `radical` ![radical][radical_repo] | 42 | | `merko` ![merko][merko_repo] | `gruvbox` ![gruvbox][gruvbox_repo] | `tokyonight` ![tokyonight][tokyonight_repo] | 43 | | `onedark` ![onedark][onedark_repo] | `cobalt` ![cobalt][cobalt_repo] | `synthwave` ![synthwave][synthwave_repo] | 44 | | `highcontrast` ![highcontrast][highcontrast_repo] | `dracula` ![dracula][dracula_repo] | `prussian` ![prussian][prussian_repo] | 45 | | `monokai` ![monokai][monokai_repo] | `vue` ![vue][vue_repo] | `vue-dark` ![vue-dark][vue-dark_repo] | 46 | | `shades-of-purple` ![shades-of-purple][shades-of-purple_repo] | `nightowl` ![nightowl][nightowl_repo] | `buefy` ![buefy][buefy_repo] | 47 | | `blue-green` ![blue-green][blue-green_repo] | `algolia` ![algolia][algolia_repo] | `great-gatsby` ![great-gatsby][great-gatsby_repo] | 48 | | `darcula` ![darcula][darcula_repo] | `bear` ![bear][bear_repo] | `solarized-dark` ![solarized-dark][solarized-dark_repo] | 49 | | `solarized-light` ![solarized-light][solarized-light_repo] | `chartreuse-dark` ![chartreuse-dark][chartreuse-dark_repo] | `nord` ![nord][nord_repo] | 50 | | `gotham` ![gotham][gotham_repo] | `material-palenight` ![material-palenight][material-palenight_repo] | `graywhite` ![graywhite][graywhite_repo] | 51 | | `vision-friendly-dark` ![vision-friendly-dark][vision-friendly-dark_repo] | `ayu-mirage` ![ayu-mirage][ayu-mirage_repo] | `midnight-purple` ![midnight-purple][midnight-purple_repo] | 52 | | `calm` ![calm][calm_repo] | `flag-india` ![flag-india][flag-india_repo] | `omni` ![omni][omni_repo] | 53 | | `react` ![react][react_repo] | `jolly` ![jolly][jolly_repo] | `maroongold` ![maroongold][maroongold_repo] | 54 | | `yeblu` ![yeblu][yeblu_repo] | `blueberry` ![blueberry][blueberry_repo] | `slateorange` ![slateorange][slateorange_repo] | 55 | | `kacho_ga` ![kacho_ga][kacho_ga_repo] | `outrun` ![outrun][outrun_repo] | [Add your theme][add-theme] | 56 | 57 | 58 | [default]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=default 59 | [default_repocard]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=default_repocard 60 | [dark]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=dark 61 | [radical]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=radical 62 | [merko]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=merko 63 | [gruvbox]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=gruvbox 64 | [tokyonight]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=tokyonight 65 | [onedark]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=onedark 66 | [cobalt]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=cobalt 67 | [synthwave]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=synthwave 68 | [highcontrast]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=highcontrast 69 | [dracula]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=dracula 70 | [prussian]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=prussian 71 | [monokai]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=monokai 72 | [vue]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=vue 73 | [vue-dark]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=vue-dark 74 | [shades-of-purple]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=shades-of-purple 75 | [nightowl]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=nightowl 76 | [buefy]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=buefy 77 | [blue-green]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=blue-green 78 | [algolia]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=algolia 79 | [great-gatsby]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=great-gatsby 80 | [darcula]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=darcula 81 | [bear]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=bear 82 | [solarized-dark]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=solarized-dark 83 | [solarized-light]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=solarized-light 84 | [chartreuse-dark]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=chartreuse-dark 85 | [nord]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=nord 86 | [gotham]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=gotham 87 | [material-palenight]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=material-palenight 88 | [graywhite]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=graywhite 89 | [vision-friendly-dark]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=vision-friendly-dark 90 | [ayu-mirage]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=ayu-mirage 91 | [midnight-purple]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=midnight-purple 92 | [calm]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=calm 93 | [flag-india]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=flag-india 94 | [omni]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=omni 95 | [react]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=react 96 | [jolly]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=jolly 97 | [maroongold]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=maroongold 98 | [yeblu]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=yeblu 99 | [blueberry]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=blueberry 100 | [slateorange]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=slateorange 101 | [kacho_ga]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=kacho_ga 102 | [outrun]: https://github-readme-stats.vercel.app/api?username=SrGobi&show_icons=true&hide=contribs,prs&cache_seconds=86400&theme=outrun 103 | 104 | 105 | [default_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=default 106 | [default_repocard_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=default_repocard 107 | [dark_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=dark 108 | [radical_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=radical 109 | [merko_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=merko 110 | [gruvbox_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=gruvbox 111 | [tokyonight_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=tokyonight 112 | [onedark_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=onedark 113 | [cobalt_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=cobalt 114 | [synthwave_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=synthwave 115 | [highcontrast_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=highcontrast 116 | [dracula_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=dracula 117 | [prussian_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=prussian 118 | [monokai_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=monokai 119 | [vue_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=vue 120 | [vue-dark_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=vue-dark 121 | [shades-of-purple_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=shades-of-purple 122 | [nightowl_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=nightowl 123 | [buefy_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=buefy 124 | [blue-green_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=blue-green 125 | [algolia_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=algolia 126 | [great-gatsby_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=great-gatsby 127 | [darcula_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=darcula 128 | [bear_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=bear 129 | [solarized-dark_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=solarized-dark 130 | [solarized-light_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=solarized-light 131 | [chartreuse-dark_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=chartreuse-dark 132 | [nord_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=nord 133 | [gotham_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=gotham 134 | [material-palenight_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=material-palenight 135 | [graywhite_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=graywhite 136 | [vision-friendly-dark_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=vision-friendly-dark 137 | [ayu-mirage_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=ayu-mirage 138 | [midnight-purple_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=midnight-purple 139 | [calm_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=calm 140 | [flag-india_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=flag-india 141 | [omni_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=omni 142 | [react_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=react 143 | [jolly_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=jolly 144 | [maroongold_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=maroongold 145 | [yeblu_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=yeblu 146 | [blueberry_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=blueberry 147 | [slateorange_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=slateorange 148 | [kacho_ga_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=kacho_ga 149 | [outrun_repo]: https://github-readme-stats.vercel.app/api/pin/?username=SrGobi&repo=github-readme-stats&cache_seconds=86400&theme=outrun 150 | 151 | 152 | [add-theme]: https://github.com/SrGobi/github-readme-stats/edit/master/themes/index.js 153 | 154 | Wanted to add a new theme? Consider reading the [contribution guidelines](../CONTRIBUTING.md#themes-contribution) :D 155 | -------------------------------------------------------------------------------- /themes/index.js: -------------------------------------------------------------------------------- 1 | const themes = { 2 | default: { 3 | title_color: "2f80ed", 4 | icon_color: "4c71f2", 5 | text_color: "333", 6 | bg_color: "fffefe", 7 | }, 8 | default_repocard: { 9 | title_color: "2f80ed", 10 | icon_color: "586069", // icon color is different 11 | text_color: "333", 12 | bg_color: "fffefe", 13 | }, 14 | dark: { 15 | title_color: "fff", 16 | icon_color: "79ff97", 17 | text_color: "9f9f9f", 18 | bg_color: "151515", 19 | }, 20 | radical: { 21 | title_color: "fe428e", 22 | icon_color: "f8d847", 23 | text_color: "a9fef7", 24 | bg_color: "141321", 25 | }, 26 | merko: { 27 | title_color: "abd200", 28 | icon_color: "b7d364", 29 | text_color: "68b587", 30 | bg_color: "0a0f0b", 31 | }, 32 | gruvbox: { 33 | title_color: "fabd2f", 34 | icon_color: "fe8019", 35 | text_color: "8ec07c", 36 | bg_color: "282828", 37 | }, 38 | tokyonight: { 39 | title_color: "70a5fd", 40 | icon_color: "bf91f3", 41 | text_color: "38bdae", 42 | bg_color: "1a1b27", 43 | }, 44 | onedark: { 45 | title_color: "e4bf7a", 46 | icon_color: "8eb573", 47 | text_color: "df6d74", 48 | bg_color: "282c34", 49 | }, 50 | cobalt: { 51 | title_color: "e683d9", 52 | icon_color: "0480ef", 53 | text_color: "75eeb2", 54 | bg_color: "193549", 55 | }, 56 | synthwave: { 57 | title_color: "e2e9ec", 58 | icon_color: "ef8539", 59 | text_color: "e5289e", 60 | bg_color: "2b213a", 61 | }, 62 | highcontrast: { 63 | title_color: "e7f216", 64 | icon_color: "00ffff", 65 | text_color: "fff", 66 | bg_color: "000", 67 | }, 68 | dracula: { 69 | title_color: "ff6e96", 70 | icon_color: "79dafa", 71 | text_color: "f8f8f2", 72 | bg_color: "282a36", 73 | }, 74 | prussian: { 75 | title_color: "bddfff", 76 | icon_color: "38a0ff", 77 | text_color: "6e93b5", 78 | bg_color: "172f45", 79 | }, 80 | monokai: { 81 | title_color: "eb1f6a", 82 | icon_color: "e28905", 83 | text_color: "f1f1eb", 84 | bg_color: "272822", 85 | }, 86 | vue: { 87 | title_color: "41b883", 88 | icon_color: "41b883", 89 | text_color: "273849", 90 | bg_color: "fffefe", 91 | }, 92 | "vue-dark": { 93 | title_color: "41b883", 94 | icon_color: "41b883", 95 | text_color: "fffefe", 96 | bg_color: "273849", 97 | }, 98 | "shades-of-purple": { 99 | title_color: "fad000", 100 | icon_color: "b362ff", 101 | text_color: "a599e9", 102 | bg_color: "2d2b55", 103 | }, 104 | nightowl: { 105 | title_color: "c792ea", 106 | icon_color: "ffeb95", 107 | text_color: "7fdbca", 108 | bg_color: "011627", 109 | }, 110 | buefy: { 111 | title_color: "7957d5", 112 | icon_color: "ff3860", 113 | text_color: "363636", 114 | bg_color: "ffffff", 115 | }, 116 | "blue-green": { 117 | title_color: "2f97c1", 118 | icon_color: "f5b700", 119 | text_color: "0cf574", 120 | bg_color: "040f0f", 121 | }, 122 | algolia: { 123 | title_color: "00AEFF", 124 | icon_color: "2DDE98", 125 | text_color: "FFFFFF", 126 | bg_color: "050F2C", 127 | }, 128 | "great-gatsby": { 129 | title_color: "ffa726", 130 | icon_color: "ffb74d", 131 | text_color: "ffd95b", 132 | bg_color: "000000", 133 | }, 134 | darcula: { 135 | title_color: "BA5F17", 136 | icon_color: "84628F", 137 | text_color: "BEBEBE", 138 | bg_color: "242424", 139 | }, 140 | bear: { 141 | title_color: "e03c8a", 142 | icon_color: "00AEFF", 143 | text_color: "bcb28d", 144 | bg_color: "1f2023", 145 | }, 146 | "solarized-dark": { 147 | title_color: "268bd2", 148 | icon_color: "b58900", 149 | text_color: "859900", 150 | bg_color: "002b36", 151 | }, 152 | "solarized-light": { 153 | title_color: "268bd2", 154 | icon_color: "b58900", 155 | text_color: "859900", 156 | bg_color: "fdf6e3", 157 | }, 158 | "chartreuse-dark": { 159 | title_color: "7fff00", 160 | icon_color: "00AEFF", 161 | text_color: "fff", 162 | bg_color: "000", 163 | }, 164 | nord: { 165 | title_color: "81a1c1", 166 | text_color: "d8dee9", 167 | icon_color: "88c0d0", 168 | bg_color: "2e3440", 169 | }, 170 | gotham: { 171 | title_color: "2aa889", 172 | icon_color: "599cab", 173 | text_color: "99d1ce", 174 | bg_color: "0c1014", 175 | }, 176 | "material-palenight": { 177 | title_color: "c792ea", 178 | icon_color: "89ddff", 179 | text_color: "a6accd", 180 | bg_color: "292d3e", 181 | }, 182 | graywhite: { 183 | title_color: "24292e", 184 | icon_color: "24292e", 185 | text_color: "24292e", 186 | bg_color: "ffffff", 187 | }, 188 | "vision-friendly-dark": { 189 | title_color: "ffb000", 190 | icon_color: "785ef0", 191 | text_color: "ffffff", 192 | bg_color: "000000", 193 | }, 194 | "ayu-mirage": { 195 | title_color: "f4cd7c", 196 | icon_color: "73d0ff", 197 | text_color: "c7c8c2", 198 | bg_color: "1f2430", 199 | }, 200 | "midnight-purple": { 201 | title_color: "9745f5", 202 | icon_color: "9f4bff", 203 | text_color: "ffffff", 204 | bg_color: "000000", 205 | }, 206 | calm: { 207 | title_color: "e07a5f", 208 | icon_color: "edae49", 209 | text_color: "ebcfb2", 210 | bg_color: "373f51", 211 | }, 212 | "flag-india": { 213 | title_color: "ff8f1c", 214 | icon_color: "250E62", 215 | text_color: "509E2F", 216 | bg_color: "ffffff", 217 | }, 218 | omni: { 219 | title_color: "FF79C6", 220 | icon_color: "e7de79", 221 | text_color: "E1E1E6", 222 | bg_color: "191622", 223 | }, 224 | react: { 225 | title_color: "61dafb", 226 | icon_color: "61dafb", 227 | text_color: "ffffff", 228 | bg_color: "20232a", 229 | }, 230 | jolly: { 231 | title_color: "ff64da", 232 | icon_color: "a960ff", 233 | text_color: "ffffff", 234 | bg_color: "291B3E", 235 | }, 236 | maroongold: { 237 | title_color: "F7EF8A", 238 | icon_color: "F7EF8A", 239 | text_color: "E0AA3E", 240 | bg_color: "260000", 241 | }, 242 | yeblu: { 243 | title_color: "ffff00", 244 | icon_color: "ffff00", 245 | text_color: "ffffff", 246 | bg_color: "002046", 247 | }, 248 | blueberry: { 249 | title_color: "82aaff", 250 | icon_color: "89ddff", 251 | text_color: "27e8a7", 252 | bg_color: "242938", 253 | }, 254 | slateorange: { 255 | title_color: "faa627", 256 | icon_color: "faa627", 257 | text_color: "ffffff", 258 | bg_color: "36393f", 259 | }, 260 | kacho_ga: { 261 | title_color: "bf4a3f", 262 | icon_color: "a64833", 263 | text_color: "d9c8a9", 264 | bg_color: "402b23", 265 | }, 266 | outrun:{ 267 | title_color: "ffcc00", 268 | icon_color: "ff1aff", 269 | text_color: "8080ff", 270 | bg_color: "141439", 271 | } 272 | }; 273 | 274 | module.exports = themes; 275 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "redirects": [ 3 | { 4 | "source": "/", 5 | "destination": "https://github.com/SrGobi/github-readme-stats" 6 | } 7 | ] 8 | } 9 | --------------------------------------------------------------------------------