├── 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 |
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 | [](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 | 
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 | 
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 | 
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 | 
57 | ```
58 |
59 | #### Todos los temas incorporados :-
60 |
61 | dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula
62 |
63 |
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 | [](https://github.com/SrGobi/github-readme-stats)
148 | ```
149 |
150 | ### Demo
151 |
152 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](https://github.com/SrGobi/github-readme-stats)
204 | ```
205 |
206 | ### Demo
207 |
208 | [](https://github.com/SrGobi/github-readme-stats)
209 |
210 | - Disposición compacta
211 |
212 | [](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 | [](https://github.com/SrGobi/github-readme-stats)
220 | ```
221 |
222 | ### Demo
223 |
224 | [](https://github.com/SrGobi/github-readme-stats)
225 |
226 | [](https://github.com/SrGobi/github-readme-stats)
227 |
228 | - Disposición compacta
229 |
230 | [](https://github.com/SrGobi/github-readme-stats)
231 |
232 | ---
233 |
234 | ### Todas las demostraciones
235 |
236 | - Defecto
237 |
238 | 
239 |
240 | - Ocultar estadísticas específicas
241 |
242 | 
243 |
244 | - Mostrando iconos
245 |
246 | 
247 |
248 | - Incluir todas las confirmaciones
249 |
250 | 
251 |
252 | - Temas
253 |
254 | Elija entre cualquiera de los [default themes](#temas)
255 |
256 | 
257 |
258 | - Degradado
259 |
260 | 
261 |
262 | - Personalizar la tarjeta de estadísticas
263 |
264 | 
265 |
266 | - Configuración de la configuración regional de la tarjeta
267 |
268 | 
269 |
270 | - Personalización de la tarjeta de repositorio
271 |
272 | 
273 |
274 | - Idiomas principales
275 |
276 | [](https://github.com/SrGobi/github-readme-stats)
277 |
278 | - Tarjeta de Wakatime
279 |
280 | [](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 | 
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})
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 |
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 |
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 |
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 |
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 |
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 |
65 | `;
66 |
67 | const prefixIcon = `
68 |
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 |
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 |
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 |
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 |
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 |
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 | 
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 |
--------------------------------------------------------------------------------