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

```
-### Ajouter le compte des contributions privées au compte des commits totaux
-
-Vous pouvez ajouter le compte de toutes vos contributions privées au compte total des engagements en utilisant le paramètre de requête `?count_private=true`.
-
-_Note: Si vous déployez vous-même ce projet, les contributions privées seront comptées par défaut ; sinon, vous devez choisir de partager les comptes de vos contributions privées._
-
-> Options: `&count_private=true`
-
-```md
-
-```
-
### Afficher les icônes
Pour activer les icônes, vous pouvez passer `show_icons=true` dans le paramètre de requête, comme ceci :
@@ -138,7 +128,7 @@ Vous pouvez personnaliser l'apparence de votre `Carte des stats` ou `Carte de d
- `bg_color` - Couleur du fond de la carte _(hex color)_ **ou** un gradiant de la forme _angle,start,end_
- `hide_border` - Cache la bordure de la carte _(booléen)_
- `theme` - Nom du thème, parmis [tous les thèmes disponibles](../themes/README.md)
-- `cache_seconds` - Paramétrer le cache manuellement _(min: 1800, max: 86400)_
+- `cache_seconds` - Paramétrer le cache manuellement _(min: 14400, max: 86400)_
- `locale` - définir la langue de la carte _(par exemple. cn, de, es, etc.)_
##### Gradient in bg_color
@@ -158,7 +148,7 @@ Vous pouvez fournir plusieurs valeurs (suivie d'une virgule) dans l'option bg_co
- `hide_rank` - Masquer le rang _(boolean)_
- `show_icons` - Afficher les icônes _(boolean)_
- `include_all_commits` - Compter le total de commits au lieu de ne compter que les commits de l'année en cours _(boolean)_
-- `count_private` - Compter les commits privés _(boolean)_
+- `count_private` - Compter les contributions privées _(boolean)_
- `line_height` - Fixer la hauteur de la ligne entre les textes _(number)_
#### Repo Card Exclusive Options:
@@ -169,7 +159,7 @@ Vous pouvez fournir plusieurs valeurs (suivie d'une virgule) dans l'option bg_co
- `hide` - Masquer les langages spécifiés sur la carte _(Comma seperated values)_
- `hide_title` - Masquer le titre _(boolean)_
-- `layout` - Alterner entre 2 mise en page `default` & `compact`
+- `layout` - Alterner entre 5 mise en page `normal` & `compact` & `donut` & `donut-vertical` & `pie`
- `card_width` - Fixer la largeur de la carte manuellement _(number)_
> :warning: **Important:**
diff --git a/docs/readme_it.md b/docs/readme_it.md
index 1b2df96a5044d0..f1c20d1a48d16d 100644
--- a/docs/readme_it.md
+++ b/docs/readme_it.md
@@ -53,6 +53,8 @@
Nederlands
.
नेपाली
+ .
+ Türkçe
Se ti piace questo progetto, considera la possibilità di donare per aiutare a renderlo migliore!
@@ -138,7 +140,7 @@ Puoi personalizzare l'aspetto delle tue `Stats Card` o delle `Repo Card` in qual
- `bg_color` - Colore dello sfondo _(in esadecimale)_ **oppure** un gradiente nella forma _angolo,inizio,fine_
- `hide_border` - Nasconde il bordo della carta _(booleano)_
- `theme` - Nome del tema, dai un'occhiata a [tutti i temi disponibili](../themes/README.md)
-- `cache_seconds` - Specifica manualmente il valore di cache, in secondi _(min: 1800, max: 86400)_
+- `cache_seconds` - Specifica manualmente il valore di cache, in secondi _(min: 14400, max: 86400)_
- `locale` - Impostare la lingua nella scheda _(per esempio. cn, de, es, eccetera.)_
##### Gradiente nello sfondo
@@ -169,7 +171,7 @@ Puoi fornire valori separati da virgola nel parametro bg_color per creare un gra
- `hide` - Nasconde un linguaggio specifico _(valori separati da virgola)_
- `hide_title` - Nasconde il titolo _(booleano)_
-- `layout` - Specifica il tipo di layout, `default` (esteso) o `compact` (compatto)
+- `layout` - Specificare il tipo di layout, `normal` (esteso), `compact` (compatto), `donut` (ciambella), `donut-vertical` (ciambella verticale) e `pie` (torta)
- `card_width` - Specifica il valore della larghezza _(numero)_
> :warning: **Importante:**
diff --git a/docs/readme_ja.md b/docs/readme_ja.md
index b00c77a7712a1f..4b1dcd2bcf314c 100644
--- a/docs/readme_ja.md
+++ b/docs/readme_ja.md
@@ -53,6 +53,8 @@
Nederlands
.
नेपाली
+ .
+ Türkçe
기능들이 마음에 드시나요? 괜찮으시다면, 서비스 개선을 위해 기부를 고려해주세요!
@@ -149,7 +151,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr
- `bg_color` - 카드의 배경 색상 _(hex color)_ **혹은** 다음 양식으로 그라데이션 주기 _angle,start,end_
- `hide_border` - 카드의 테두리 표시 여부 _(boolean)_
- `theme` - 테마의 이름, [사용 가능한 모든 테마](../themes/README.md) 에서 선택
-- `cache_seconds` - 수동으로 캐시 헤더 설정 _(min: 1800, max: 86400)_
+- `cache_seconds` - 수동으로 캐시 헤더 설정 _(min: 14400, max: 86400)_
- `locale` - 카드에 표시할 언어 _(e.g. kr, cn, de, es, etc.)_
##### 배경에 그라데이션 주기
@@ -187,7 +189,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr
- `hide` - 카드에서 특정 언어 제외 _(Comma-separated values)_
- `hide_title` - 타이틀 제외 _(boolean)_
-- `layout` - 사용 가능한 두 가지 값, `default` & `compact` 중 표시 형태 선택
+- `layout` - 5가지 값 사용 가능, `normal` & `compact` & `donut` & `donut-vertical` & `pie` 중 표시 형태 선택
- `card_width` - 카드 너비 직접 설정 _(number)_
- `langs_count` - 카드에 표시할 언어의 수 (1-10 사이, 기본 값 : 5) _(number)_
- `exclude_repo` - 통계에 제외할 저장소 지정 _(Comma-separated values)_
@@ -296,18 +298,18 @@ _참고:
`?username=` 속성의 값을 [Wakatime](https://wakatime.com) 계정의 사용자 명(닉네임)으로 바꿔주세요.
```md
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
```
### 미리보기
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
- 컴팩트한 레이아웃
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
---
@@ -357,7 +359,7 @@ _참고:
- Wakatime 카드
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
---
diff --git a/docs/readme_nl.md b/docs/readme_nl.md
index 597f0c86445e36..8d38d06adbcd11 100644
--- a/docs/readme_nl.md
+++ b/docs/readme_nl.md
@@ -53,6 +53,8 @@
Nederlands
.
नेपाली
+ .
+ Türkçe
Bevalt het project? Doneer om het te verbeteren!
@@ -142,7 +144,7 @@ Je kan het uiterlijk van je `Statistieken kaart` of `Repo kaart` aanpassen hoe j
- `bg_color` - Achtergrond kleur van de kaart _(hex kleur)_ **of** een verloop van kleuren in het formaat van _graden,start,einde_
- `hide_border` - Verbergt de rand van de kaart _(boolean)_
- `theme` - Naam van het thema, kies uit [alle beschikbare thema\'s](../themes/README.md)
-- `cache_seconds` - Stel de cache header handmatig in _(min: 1800, max: 86400)_
+- `cache_seconds` - Stel de cache header handmatig in _(min: 14400, max: 86400)_
- `locale` - Stel taal van de kaart in _(e.g. cn, de, es, etc.)_
##### Kleurenverloop in bg_color (achtergrond kleur):
@@ -174,7 +176,7 @@ Je kan meerdere komma verdeelde waarden in de bg_color optie geven om een kleure
- `hide` - Verbergt specifieke talen van de kaart _(komma gescheiden waardes)_
- `hide_title` - _(boolean)_
-- `layout` - Keuze voor de twee beschikbare layouts `default` & `compact`
+- `layout` - Kies uit de vijf beschikbare lay-outs `normal` & `compact` & `donut` & `donut-vertical` & `pie`
- `card_width` - Stelt de breedte van de kaart handmatig in. _(nummer)_
- `langs_count` - Laat meer talen op de kaart zien, waarde tussen 1-10, staat standaard op to 5 _(nummer)_
- `exclude_repo` - Verbergt specifieke repositories _(komma gescheiden waardes)_
@@ -281,14 +283,14 @@ Je kan de `&layout=compact` optie gebruiken om het kaart ontwerp aan te passen.
Verander de `?username=` waarde naar je [Wakatime](https://wakatime.com) gebruikersnaam.
```md
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
```
### Demo
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
---
@@ -338,7 +340,7 @@ Kies uit de [standaard thema\'s](#themes)
- Wakatime kaart
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
---
diff --git a/docs/readme_np.md b/docs/readme_np.md
index 654427fbda6549..e4914263019034 100644
--- a/docs/readme_np.md
+++ b/docs/readme_np.md
@@ -49,8 +49,12 @@
Italiano
·
한국어
+ .
+ Nederlands
·
नेपाली
+ .
+ Türkçe
परियोजना मनपर्यो? तपाईं मद्दत गर्न सक्नुहुन्छ यो परियोजना बढ्न
@@ -138,7 +142,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr
- `bg_color` - Card's background color _(hex color)_ **or** a gradient in the form of _angle,start,end_
- `hide_border` - Hides the card's border _(boolean)_
- `theme` - name of the theme, choose from [all available themes](./themes/README.md)
-- `cache_seconds` - set the cache header manually _(min: 1800, max: 86400)_
+- `cache_seconds` - set the cache header manually _(min: 14400, max: 86400)_
- `locale` - set the language in the card _(e.g. cn, de, es, etc.)_
##### Gradient in bg_color
@@ -170,7 +174,7 @@ You can provide multiple comma-separated values in bg_color option to render a g
- `hide` - Hide the languages specified from the card _(Comma-separated values)_
- `hide_title` - _(boolean)_
-- `layout` - Switch between two available layouts `default` & `compact`
+- `layout` - Switch between five available layouts `normal` & `compact` & `donut` & `donut-vertical` & `pie`. Default: `normal`.
- `card_width` - Set the card's width manually _(number)_
- `langs_count` - Show more languages on the card, between 1-10, defaults to 5 _(number)_
- `exclude_repo` - Exclude specified repositories _(Comma-separated values)_
@@ -275,14 +279,14 @@ You can use the `&langs_count=` option to increase or decrease the number of lan
Change the `?username=` value to your [Wakatime](https://wakatime.com) username.
```md
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
```
### डेमो
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
---
@@ -332,7 +336,7 @@ Change the `?username=` value to your [Wakatime](https://wakatime.com) username.
- वक समय कार्ड
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
---
diff --git a/docs/readme_pt-BR.md b/docs/readme_pt-BR.md
index 62c23dc55c7e2b..a590440ef69ec0 100644
--- a/docs/readme_pt-BR.md
+++ b/docs/readme_pt-BR.md
@@ -141,7 +141,7 @@ Personalize a aparência do seu `Stats Card` ou `Repo Card` da maneira que desej
- `bg_color` - Cor de fundo do cartão _(hex color)_
- `hide_border` - Esconde a borda do cartão _(boleano)_
- `theme` - Nome do tema, escolha em [todos os temas disponíveis](../themes/README.md)
-- `cache_seconds` - Defina o cabeçalho do cache manualmente _(min: 1800, max: 86400)_
+- `cache_seconds` - Defina o cabeçalho do cache manualmente _(min: 14400, max: 86400)_
- `locale` - defina o idioma no cartão _(por exemplo. cn, de, es, etc.)_
> Nota sobre o cache: Cartões de repositório tem um cache padrão de 30 minutos (1800 segundos), se o número a contagem de forks e contagem de estrelas é menor que 1 mil o padrão é 2 horas (7200 segundos). Note também que o cache é limitado a um mínimo de 30 minutos e um máximo de 24 horas.
@@ -164,7 +164,7 @@ Personalize a aparência do seu `Stats Card` ou `Repo Card` da maneira que desej
- `hide` - Oculta linguagens específicas _(Valores separados por vírgulas)_
- `hide_title` - Oculta o título _(boolean)_
-- `layout` - Alterna entre os dois layouts disponíveis `default` & `compact`
+- `layout` - Alternar entre os cinco layouts disponíveis `normal` & `compact` & `donut` & `donut-vertical` & `pie`
- `card_width` - Define a largura do cartão manualmente _(number)_
> :warning: **Importante:**
@@ -242,14 +242,14 @@ Utilize a opção `&layout=compact` para mudar o layout do cartão.
Altere o valor de `?username=` para o seu username do Wakatime.
```md
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
```
### Demonstração
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
---
diff --git a/docs/readme_tr.md b/docs/readme_tr.md
index 7b11cf3706f1e1..9ee358b97bdece 100644
--- a/docs/readme_tr.md
+++ b/docs/readme_tr.md
@@ -143,7 +143,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr
- `bg_color` - Kartın arkaplan rengi _(hex color / hex rengi)_ **ya da** gradient şeklinde _açı,başlangıç,bitiş_
- `hide_border` - Kartın çerçevelerini gizler _(boolean)_
- `theme` - Temanın rengi [tüm temalar](./themes/README.md)
-- `cache_seconds` - Manuel olarak cache'i belirleyebilirsiniz _(en az: 1800, en fazla: 86400)_
+- `cache_seconds` - Manuel olarak cache'i belirleyebilirsiniz _(en az: 14400, en fazla: 86400)_
- `locale` - Karttaki dili seçebilirsiniz _(örneğin; tr, cn, de, es, vb.)_
##### bg_color'da Gradient
@@ -176,7 +176,7 @@ bg_color içerisinde birden fazla rengi gradient olarak göstermek için virgül
- `hide` - Belirli bir dili listede gizler _(Virgül ile ayırılmış değerlerle)_
- `hide_title` - _(boolean)_
-- `layout` - Uygun olan iki tasarım / layout arasında değişiklik yapar `default` & `compact`
+- `layout` - Beş uygun tasarım / düzen arasında geçiş yapın `normal` & `compact` & `donut` & `donut-vertical` & `pie`
- `card_width` - Kartın genişliğini manuel olarak belirler _(number)_
- `langs_count` - 1-10 arasında istediğiniz kadar dil gösterebilirsiniz. Varsayılan: 5 _(number)_
- `exclude_repo` - Belirli repoları listeden çıkartır _(Virgül ile ayırılmış değerlerle)_
@@ -282,18 +282,18 @@ Endpoint: `api/top-langs?username=mustafacagri`
`?username=` değerini [Wakatime](https://wakatime.com)'daki kullanıcı adınızla değiştirin.
```md
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
```
### Demo
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
- Kompakt Düzen
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
---
@@ -343,7 +343,7 @@ Endpoint: `api/top-langs?username=mustafacagri`
- Wakatime kart
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
---
@@ -409,7 +409,7 @@ Teşekkürler! :heart:
---
-[](https://vercel.com?utm_source=github_readme_stats_team&utm_campaign=oss)
+[](https://vercel.com?utm_source=github_readme_stats_team&utm_campaign=oss)
Katkılara açığız! <3
diff --git a/express.js b/express.js
new file mode 100644
index 00000000000000..6ce92ff0351817
--- /dev/null
+++ b/express.js
@@ -0,0 +1,15 @@
+import statsCard from "./api/index.js";
+import repoCard from "./api/pin.js";
+import langCard from "./api/top-langs.js";
+import wakatimeCard from "./api/wakatime.js";
+import express from "express";
+import dotenv from "dotenv";
+
+dotenv.config();
+const app = express();
+app.listen(process.env.port || 9000);
+
+app.get("/", statsCard);
+app.get("/pin", repoCard);
+app.get("/top-langs", langCard);
+app.get("/wakatime", wakatimeCard);
diff --git a/package-lock.json b/package-lock.json
index 048c316bfde58f..a2156910681da0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,7 +22,7 @@
"@testing-library/dom": "^8.17.1",
"@testing-library/jest-dom": "^5.16.5",
"@uppercod/css-to-object": "^1.1.1",
- "axios-mock-adapter": "^1.18.1",
+ "axios-mock-adapter": "^1.21.2",
"color-contrast-checker": "^2.1.0",
"hjson": "^3.2.2",
"husky": "^8.0.0",
@@ -1495,31 +1495,19 @@
}
},
"node_modules/acorn-globals": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz",
- "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==",
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz",
+ "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==",
"dev": true,
"dependencies": {
- "acorn": "^7.1.1",
- "acorn-walk": "^7.1.1"
- }
- },
- "node_modules/acorn-globals/node_modules/acorn": {
- "version": "7.4.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
- "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
- "dev": true,
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "node": ">=0.4.0"
+ "acorn": "^8.1.0",
+ "acorn-walk": "^8.0.2"
}
},
"node_modules/acorn-walk": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
- "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
+ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
"dev": true,
"engines": {
"node": ">=0.4.0"
@@ -1778,12 +1766,6 @@
"node": ">=8"
}
},
- "node_modules/browser-process-hrtime": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
- "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==",
- "dev": true
- },
"node_modules/browserslist": {
"version": "4.21.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz",
@@ -2509,20 +2491,6 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
- "node_modules/fsevents": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
- "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
- "dev": true,
- "hasInstallScript": true,
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -3865,18 +3833,18 @@
}
},
"node_modules/jsdom": {
- "version": "20.0.0",
- "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.0.tgz",
- "integrity": "sha512-x4a6CKCgx00uCmP+QakBDFXwjAJ69IkkIWHmtmjd3wvXPcdOS44hfX2vqkOQrVrq8l9DhNNADZRXaCEWvgXtVA==",
+ "version": "20.0.1",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.1.tgz",
+ "integrity": "sha512-pksjj7Rqoa+wdpkKcLzQRHhJCEE42qQhl/xLMUKHgoSejaKOdaXEAnqs6uDNwMl/fciHTzKeR8Wm8cw7N+g98A==",
"dev": true,
"dependencies": {
"abab": "^2.0.6",
- "acorn": "^8.7.1",
- "acorn-globals": "^6.0.0",
+ "acorn": "^8.8.0",
+ "acorn-globals": "^7.0.0",
"cssom": "^0.5.0",
"cssstyle": "^2.3.0",
"data-urls": "^3.0.2",
- "decimal.js": "^10.3.1",
+ "decimal.js": "^10.4.1",
"domexception": "^4.0.0",
"escodegen": "^2.0.0",
"form-data": "^4.0.0",
@@ -3884,18 +3852,17 @@
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.1",
"is-potential-custom-element-name": "^1.0.1",
- "nwsapi": "^2.2.0",
- "parse5": "^7.0.0",
+ "nwsapi": "^2.2.2",
+ "parse5": "^7.1.1",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
- "tough-cookie": "^4.0.0",
- "w3c-hr-time": "^1.0.2",
+ "tough-cookie": "^4.1.2",
"w3c-xmlserializer": "^3.0.0",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^2.0.0",
"whatwg-mimetype": "^3.0.0",
"whatwg-url": "^11.0.0",
- "ws": "^8.8.0",
+ "ws": "^8.9.0",
"xml-name-validator": "^4.0.0"
},
"engines": {
@@ -5397,15 +5364,6 @@
"node": ">=10.12.0"
}
},
- "node_modules/w3c-hr-time": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
- "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==",
- "dev": true,
- "dependencies": {
- "browser-process-hrtime": "^1.0.0"
- }
- },
"node_modules/w3c-xmlserializer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz",
@@ -5581,9 +5539,9 @@
"dev": true
},
"node_modules/yaml": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz",
- "integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==",
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz",
+ "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==",
"dev": true,
"engines": {
"node": ">= 14"
@@ -6830,27 +6788,19 @@
"dev": true
},
"acorn-globals": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz",
- "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==",
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz",
+ "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==",
"dev": true,
"requires": {
- "acorn": "^7.1.1",
- "acorn-walk": "^7.1.1"
- },
- "dependencies": {
- "acorn": {
- "version": "7.4.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
- "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
- "dev": true
- }
+ "acorn": "^8.1.0",
+ "acorn-walk": "^8.0.2"
}
},
"acorn-walk": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
- "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
+ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
"dev": true
},
"agent-base": {
@@ -7049,12 +6999,6 @@
"fill-range": "^7.0.1"
}
},
- "browser-process-hrtime": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
- "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==",
- "dev": true
- },
"browserslist": {
"version": "4.21.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz",
@@ -7590,13 +7534,6 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
- "fsevents": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
- "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
- "dev": true,
- "optional": true
- },
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -8609,18 +8546,18 @@
}
},
"jsdom": {
- "version": "20.0.0",
- "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.0.tgz",
- "integrity": "sha512-x4a6CKCgx00uCmP+QakBDFXwjAJ69IkkIWHmtmjd3wvXPcdOS44hfX2vqkOQrVrq8l9DhNNADZRXaCEWvgXtVA==",
+ "version": "20.0.1",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.1.tgz",
+ "integrity": "sha512-pksjj7Rqoa+wdpkKcLzQRHhJCEE42qQhl/xLMUKHgoSejaKOdaXEAnqs6uDNwMl/fciHTzKeR8Wm8cw7N+g98A==",
"dev": true,
"requires": {
"abab": "^2.0.6",
- "acorn": "^8.7.1",
- "acorn-globals": "^6.0.0",
+ "acorn": "^8.8.0",
+ "acorn-globals": "^7.0.0",
"cssom": "^0.5.0",
"cssstyle": "^2.3.0",
"data-urls": "^3.0.2",
- "decimal.js": "^10.3.1",
+ "decimal.js": "^10.4.1",
"domexception": "^4.0.0",
"escodegen": "^2.0.0",
"form-data": "^4.0.0",
@@ -8628,18 +8565,17 @@
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.1",
"is-potential-custom-element-name": "^1.0.1",
- "nwsapi": "^2.2.0",
- "parse5": "^7.0.0",
+ "nwsapi": "^2.2.2",
+ "parse5": "^7.1.1",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
- "tough-cookie": "^4.0.0",
- "w3c-hr-time": "^1.0.2",
+ "tough-cookie": "^4.1.2",
"w3c-xmlserializer": "^3.0.0",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^2.0.0",
"whatwg-mimetype": "^3.0.0",
"whatwg-url": "^11.0.0",
- "ws": "^8.8.0",
+ "ws": "^8.9.0",
"xml-name-validator": "^4.0.0"
}
},
@@ -9724,15 +9660,6 @@
"convert-source-map": "^1.6.0"
}
},
- "w3c-hr-time": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
- "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==",
- "dev": true,
- "requires": {
- "browser-process-hrtime": "^1.0.0"
- }
- },
"w3c-xmlserializer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz",
@@ -9855,9 +9782,9 @@
"dev": true
},
"yaml": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz",
- "integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==",
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz",
+ "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==",
"dev": true
},
"yargs": {
diff --git a/package.json b/package.json
index 95b1a11dad93f3..606d4f5440a242 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,7 @@
"@testing-library/dom": "^8.17.1",
"@testing-library/jest-dom": "^5.16.5",
"@uppercod/css-to-object": "^1.1.1",
- "axios-mock-adapter": "^1.18.1",
+ "axios-mock-adapter": "^1.21.2",
"color-contrast-checker": "^2.1.0",
"hjson": "^3.2.2",
"husky": "^8.0.0",
diff --git a/readme.md b/readme.md
index 678c5c0b14af48..e7ef2b7096b5c3 100644
--- a/readme.md
+++ b/readme.md
@@ -66,7 +66,7 @@
Love the project? Please consider donating to help it improve!
-
+
Are you considering supporting the project by donating? Please DO NOT!!
@@ -92,7 +92,9 @@ Visit and make a small donation to hel
- [Repo Card Exclusive Options](#repo-card-exclusive-options)
- [Language Card Exclusive Options](#language-card-exclusive-options)
- [Wakatime Card Exclusive Option](#wakatime-card-exclusive-options)
-- [Deploy Yourself](#deploy-on-your-own-vercel-instance)
+- [Deploy Yourself](#deploy-on-your-own)
+ - [On Vercel](#on-vercel)
+ - [On other platforms](#on-other-platforms)
- [Keep your fork up to date](#keep-your-fork-up-to-date)
# GitHub Stats Card
@@ -118,22 +120,9 @@ You can pass a query parameter `&hide=` to hide any specific stats with comma-se

```
-### Adding private contributions count to total commits count
-
-You can add the count of all your private contributions to the total commits count by using the query parameter `&count_private=true`.
-
-> **Note**
-> If you are deploying this project yourself, the private contributions will be counted by default. If you are using the public Vercel instance, you need to choose to [share your private contributions](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-github-profile/managing-contribution-settings-on-your-profile/showing-your-private-contributions-and-achievements-on-your-profile).
-
-> Options: `&count_private=true`
-
-```md
-
-```
-
### Showing icons
-To enable icons, you can pass `show_icons=true` in the query param, like so:
+To enable icons, you can pass `&show_icons=true` in the query param, like so:
```md

@@ -181,7 +170,7 @@ We have included a `transparent` theme that has a transparent background. This t
##### Add transparent alpha channel to a themes bg_color
-You can use the `bg_color` parameter to make any of [the available themes](./themes/README.md) transparent. This is done by setting the `bg_color` to a colour with a transparent alpha channel (i.e. `bg_color=00000000`):
+You can use the `bg_color` parameter to make any of [the available themes](./themes/README.md) transparent. This is done by setting the `bg_color` to a color with a transparent alpha channel (i.e. `bg_color=00000000`):
```md

@@ -214,10 +203,10 @@ You can use [GitHub's theme context](https://github.blog/changelog/2021-11-24-sp
##### Use GitHub's new media feature
You can use [GitHub's new media feature](https://github.blog/changelog/2022-05-19-specify-theme-context-for-images-in-markdown-beta/) in HTML to specify whether to display images for light or dark themes. This is done using the HTML `` element in combination with the `prefers-color-scheme` media feature.
-
+
```html
-
@@ -233,7 +222,7 @@ You can use [GitHub's new media feature](https://github.blog/changelog/2022-05-1
:eyes: Show example
-
@@ -258,7 +247,7 @@ You can customize the appearance of your `Stats Card` or `Repo Card` however you
- `border_color` - Card's border color _(hex color)_. Default: `e4e2e2` (Does not apply when `hide_border` is enabled).
- `bg_color` - Card's background color _(hex color)_ **or** a gradient in the form of _angle,start,end_. Default: `fffefe`
- `hide_border` - Hides the card's border _(boolean)_. Default: `false`
-- `theme` - name of the theme, choose from [all available themes](./themes/README.md). Default: `default` theme.
+- `theme` - name of the theme, choose from [all available themes](./themes/README.md). Default: `default` theme.
- `cache_seconds` - set the cache header manually _(min: 14400, max: 86400)_. Default: `14400 seconds (4 hours)`.
- `locale` - set the language in the card _(e.g. cn, de, es, etc.)_. Default: `en`.
- `border_radius` - Corner rounding on the card. Default: `4.5`.
@@ -278,15 +267,17 @@ You can provide multiple comma-separated values in the bg_color option to render
- `hide_title` - _(boolean)_. Default: `false`.
- `card_width` - Set the card's width manually _(number)_. Default: `500px (approx.)`.
- `hide_rank` - _(boolean)_ hides the rank and automatically resizes the card width. Default: `false`.
+- `rank_icon` - Shows alternative rank icon (i.e. `github` or `default`). Default: `default`.
- `show_icons` - _(boolean)_. Default: `false`.
- `include_all_commits` - Count total commits instead of just the current year commits _(boolean)_. Default: `false`.
-- `count_private` - Count private commits _(boolean)_. Default: `false`.
+- `count_private` - Count private contributions _(boolean)_. Default: `false`.
- `line_height` - Sets the line height between text _(number)_. Default: `25`.
- `exclude_repo` - Exclude stars from specified repositories _(Comma-separated values)_. Default: `[] (blank array)`.
- `custom_title` - Sets a custom title for the card. Default: ` GitHub Stats`.
- `text_bold` - Use bold text _(boolean)_. Default: `true`.
- `disable_animations` - Disables all animations in the card _(boolean)_. Default: `false`.
- `ring_color` - Color of the rank circle _(hex color)_. Defaults to the theme ring color if it exists and otherwise the title color.
+- `number_format` - Switch between two available formats for displaying the card values `short` (i.e. `6.6k`) and `long` (i.e. `6626`). Default: `short`.
> **Note**
> When hide_rank=`true`, the minimum card width is 270 px + the title length and padding.
@@ -299,12 +290,15 @@ You can provide multiple comma-separated values in the bg_color option to render
- `hide` - Hide the languages specified from the card _(Comma-separated values)_. Default: `[] (blank array)`.
- `hide_title` - _(boolean)_. Default: `false`.
-- `layout` - Switch between two available layouts `default` & `compact`. Default: `default`.
+- `layout` - Switch between five available layouts `normal` & `compact` & `donut` & `donut-vertical` & `pie`. Default: `normal`.
- `card_width` - Set the card's width manually _(number)_. Default `300`.
- `langs_count` - Show more languages on the card, between 1-10 _(number)_. Default `5`.
- `exclude_repo` - Exclude specified repositories _(Comma-separated values)_. Default: `[] (blank array)`.
- `custom_title` - Sets a custom title for the card _(string)_. Default `Most Used Languages`.
- `disable_animations` - Disables all animations in the card _(boolean)_. Default: `false`.
+- `hide_progress` - It uses the compact layout option, hides percentages, and removes the bars. Default: `false`.
+- `size_weight` - Configures language stats algorithm _(number)_ (see [Language stats algorithm](#Language-stats-algorithm)), defaults to 1.
+- `count_weight` - Configures language stats algorithm _(number)_ (see [Language stats algorithm](#Language-stats-algorithm)), defaults to 0.
> **Warning**
> Language names should be URI-escaped, as specified in [Percent Encoding](https://en.wikipedia.org/wiki/Percent-encoding)
@@ -321,7 +315,6 @@ You can provide multiple comma-separated values in the bg_color option to render
- `layout` - Switch between two available layouts `default` & `compact`. Default `default`.
- `langs_count` - Limit the number of languages on the card, defaults to all reported languages _(number)_.
- `api_domain` - Set a custom API domain for the card, e.g. to use services like [Hakatime](https://github.com/mujx/hakatime) or [Wakapi](https://github.com/muety/wakapi) _(string)_. Default `Waka API`.
-- `range` – Request a range different from your WakaTime default, e.g. `last_7_days`. See [WakaTime API docs](https://wakatime.com/developers#stats) for a list of available options. _(YYYY-MM, last_7_days, last_30_days, last_6_months, last_year, or all_time)_. Default `all_time`.
* * *
@@ -354,7 +347,7 @@ Use [show_owner](#customization) variable to include the repo's owner username
The top languages card shows a GitHub user's most frequently used top language.
> **Note**
-> Top Languages does not indicate my skill level or anything like that; it's a GitHub metric to determine which languages have the most code on GitHub. It is a new feature of github-readme-stats.
+> Top Languages does not indicate the user's skill level or anything like that; it's a GitHub metric to determine which languages have the most code on GitHub. It is a new feature of github-readme-stats.
### Usage
@@ -366,6 +359,24 @@ Endpoint: `api/top-langs?username=anuraghazra`
[](https://github.com/anuraghazra/github-readme-stats)
```
+### Language stats algorithm
+
+We use the following algorithm to calculate the languages percentages on the language card:
+
+```js
+ranking_index = (byte_count ^ size_weight) * (repo_count ^ count_weight)
+```
+
+By default, only the byte count is used for determining the languages percentages shown on the language card (i.e. `size_weight=1` and `count_weight=0`). You can, however, use the `&size_weight=` and `&count_weight=` options to weight the language usage calculation. The values must be positive real numbers. [More details about the algorithm can be found here](https://github.com/anuraghazra/github-readme-stats/issues/1600#issuecomment-1046056305).
+
+- `&size_weight=1&count_weight=0` - _(default)_ Orders by byte count.
+- `&size_weight=0.5&count_weight=0.5` - _(recommended)_ Uses both byte and repo count for ranking
+- `&size_weight=0&count_weight=1` - Orders by repo count
+
+```md
+[](https://github.com/anuraghazra/github-readme-stats)
+```
+
### Exclude individual repositories
You can use the `&exclude_repo=repo1,repo2` parameter to exclude individual repositories.
@@ -398,6 +409,38 @@ You can use the `&layout=compact` option to change the card design.
[](https://github.com/anuraghazra/github-readme-stats)
```
+### Donut Chart Language Card Layout
+
+You can use the `&layout=donut` option to change the card design.
+
+```md
+[](https://github.com/anuraghazra/github-readme-stats)
+```
+
+### Donut Vertical Chart Language Card Layout
+
+You can use the `&layout=donut-vertical` option to change the card design.
+
+```md
+[](https://github.com/anuraghazra/github-readme-stats)
+```
+
+### Pie Chart Language Card Layout
+
+You can use the `&layout=pie` option to change the card design.
+
+```md
+[](https://github.com/anuraghazra/github-readme-stats)
+```
+
+### Hide Progress Bars
+
+You can use the `&hide_progress=true` option to hide the percentages and the progress bars (layout will be automatically set to `compact`).
+
+```md
+[](https://github.com/anuraghazra/github-readme-stats)
+```
+
### Demo
[](https://github.com/anuraghazra/github-readme-stats)
@@ -406,26 +449,42 @@ You can use the `&layout=compact` option to change the card design.
[](https://github.com/anuraghazra/github-readme-stats)
+- Donut Chart layout
+
+[](https://github.com/anuraghazra/github-readme-stats)
+
+- Donut Vertical Chart layout
+
+[](https://github.com/anuraghazra/github-readme-stats)
+
+- Pie Chart layout
+
+[](https://github.com/anuraghazra/github-readme-stats)
+
+- Hidden progress bars
+
+[](https://github.com/anuraghazra/github-readme-stats)
+
# Wakatime Week Stats
+> **Warning**
+> Please be aware that we currently only show data from Wakatime profiles that are public. You therefore have to make sure that **BOTH** `Display code time publicly` and `Display languages, editors, os, categories publicly` are enabled.
+
Change the `?username=` value to your [Wakatime](https://wakatime.com) username.
```md
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
```
-> **Note**:
-> Please be aware that we currently only show data from Wakatime profiles that are public.
-
### Demo
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
- Compact layout
-[](https://github.com/anuraghazra/github-readme-stats)
+[](https://github.com/anuraghazra/github-readme-stats)
* * *
@@ -443,6 +502,10 @@ Change the `?username=` value to your [Wakatime](https://wakatime.com) username.

+- Shows Github logo instead rank level
+
+
+
- Customize Border Color

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