diff --git a/api/wakatime.js b/api/wakatime.js
new file mode 100644
index 0000000000000..c68bb8f55daa1
--- /dev/null
+++ b/api/wakatime.js
@@ -0,0 +1,59 @@
+require("dotenv").config();
+const {
+ renderError,
+ parseBoolean,
+ clampValue,
+ CONSTANTS,
+} = require("../src/common/utils");
+const { fetchLast7Days } = require("../src/fetchers/wakatime-fetcher");
+const wakatimeCard = require("../src/cards/wakatime-card");
+
+module.exports = async (req, res) => {
+ const {
+ username,
+ title_color,
+ icon_color,
+ hide_border,
+ line_height,
+ text_color,
+ bg_color,
+ theme,
+ cache_seconds,
+ hide_title,
+ hide_progress,
+ } = req.query;
+
+ res.setHeader("Content-Type", "image/svg+xml");
+
+ try {
+ const last7Days = await fetchLast7Days({ username });
+
+ let cacheSeconds = clampValue(
+ parseInt(cache_seconds || CONSTANTS.TWO_HOURS, 10),
+ CONSTANTS.TWO_HOURS,
+ CONSTANTS.ONE_DAY
+ );
+
+ if (!cache_seconds) {
+ cacheSeconds = CONSTANTS.FOUR_HOURS;
+ }
+
+ res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`);
+
+ return res.send(
+ wakatimeCard(last7Days, {
+ hide_title: parseBoolean(hide_title),
+ hide_border: parseBoolean(hide_border),
+ line_height,
+ title_color,
+ icon_color,
+ text_color,
+ bg_color,
+ theme,
+ hide_progress,
+ })
+ );
+ } catch (err) {
+ return res.send(renderError(err.message, err.secondaryMessage));
+ }
+};
diff --git a/package.json b/package.json
index af82b59522d82..0cb50dc167e6e 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"dotenv": "^8.2.0",
"emoji-name-map": "^1.2.8",
"github-username-regex": "^1.0.0",
+ "prettier": "^2.1.2",
"word-wrap": "^1.2.3"
},
"husky": {
diff --git a/readme.md b/readme.md
index 2765b4d25f1ef..7ef0bef27156c 100644
--- a/readme.md
+++ b/readme.md
@@ -58,6 +58,7 @@
- [GitHub Stats Card](#github-stats-card)
- [GitHub Extra Pins](#github-extra-pins)
- [Top Languages Card](#top-languages-card)
+- [Wakatime Week Stats](#wakatime-week-stats)
- [Themes](#themes)
- [Customization](#customization)
- [Deploy Yourself](#deploy-on-your-own-vercel-instance)
@@ -171,6 +172,13 @@ You can provide multiple comma-separated values in bg_color option to render a g
> Language names should be uri-escaped, as specified in [Percent Encoding](https://en.wikipedia.org/wiki/Percent-encoding)
> (i.e: `c++` should become `c%2B%2B`, `jupyter notebook` should become `jupyter%20notebook`, etc.)
+#### Wakatime Card Exclusive Options:
+
+- `hide_title` - _(boolean)_
+- `hide_border` - _(boolean)_
+- `line_height` - Sets the line-height between text _(number)_
+- `hide_progress` - Hides the progress bar and percentage _(boolean)_
+
---
# GitHub Extra Pins
@@ -245,6 +253,20 @@ You can use the `&layout=compact` option to change the card design.
[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra&layout=compact)](https://github.com/anuraghazra/github-readme-stats)
+# Wakatime Week Stats
+
+Change the `?username=` value to your Wakatime username.
+
+```md
+[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats)
+```
+
+### Demo
+
+[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats)
+
+[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod&hide_progress=true)](https://github.com/anuraghazra/github-readme-stats)
+
---
### All Demos
@@ -287,6 +309,10 @@ Choose from any of the [default themes](#themes)
[![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra)](https://github.com/anuraghazra/github-readme-stats)
+- Wakatime card
+
+[![willianrod's wakatime stats](https://github-readme-stats.vercel.app/api/wakatime?username=willianrod)](https://github.com/anuraghazra/github-readme-stats)
+
---
### Quick Tip (Align The Repo Cards)
diff --git a/src/cards/wakatime-card.js b/src/cards/wakatime-card.js
new file mode 100644
index 0000000000000..7b0aca568fdc9
--- /dev/null
+++ b/src/cards/wakatime-card.js
@@ -0,0 +1,156 @@
+const { getCardColors, FlexLayout, clampValue } = require("../common/utils");
+const { getStyles } = require("../getStyles");
+const icons = require("../common/icons");
+const Card = require("../common/Card");
+
+const noCodingActivityNode = ({ color }) => {
+ return `
+ No coding activity this week
+ `;
+};
+
+const createProgressNode = ({
+ width,
+ color,
+ progress,
+ progressBarBackgroundColor,
+}) => {
+ const progressPercentage = clampValue(progress, 2, 100);
+
+ return `
+
+ `;
+};
+
+const createTextNode = ({
+ id,
+ label,
+ value,
+ index,
+ percent,
+ hideProgress,
+ progressBarColor,
+ progressBarBackgroundColor,
+}) => {
+ const staggerDelay = (index + 3) * 150;
+
+ const cardProgress = hideProgress
+ ? null
+ : createProgressNode({
+ progress: percent,
+ color: progressBarColor,
+ width: 220,
+ name: label,
+ progressBarBackgroundColor,
+ });
+
+ return `
+
+ ${label}:
+ ${value}
+ ${cardProgress}
+
+ `;
+};
+
+const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
+ const { languages } = stats;
+ const {
+ hide_title = false,
+ hide_border = false,
+ line_height = 25,
+ title_color,
+ icon_color,
+ text_color,
+ bg_color,
+ theme = "default",
+ hide_progress,
+ } = options;
+
+ const lheight = parseInt(line_height, 10);
+
+ // returns theme based colors with proper overrides and defaults
+ const { titleColor, textColor, iconColor, bgColor } = getCardColors({
+ title_color,
+ icon_color,
+ text_color,
+ bg_color,
+ theme,
+ });
+
+ const statItems = languages
+ ? languages
+ .filter((language) => language.hours || language.minutes)
+ .map((language) => {
+ return createTextNode({
+ id: language.name,
+ label: language.name,
+ value: language.text,
+ percent: language.percent,
+ progressBarColor: titleColor,
+ progressBarBackgroundColor: textColor,
+ hideProgress: hide_progress,
+ });
+ })
+ : [];
+
+ // Calculate the card height depending on how many items there are
+ // but if rank circle is visible clamp the minimum height to `150`
+ let height = Math.max(45 + (statItems.length + 1) * lheight, 150);
+
+ const cssStyles = getStyles({
+ titleColor,
+ textColor,
+ iconColor,
+ });
+
+ const card = new Card({
+ title: "Wakatime week stats",
+ width: 495,
+ height,
+ colors: {
+ titleColor,
+ textColor,
+ iconColor,
+ bgColor,
+ },
+ });
+
+ card.setHideBorder(hide_border);
+ card.setHideTitle(hide_title);
+ card.setCSS(
+ `
+ ${cssStyles}
+ .lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
+ `
+ );
+
+ return card.render(`
+
+ `);
+};
+
+module.exports = renderWakatimeCard;
diff --git a/src/fetchers/wakatime-fetcher.js b/src/fetchers/wakatime-fetcher.js
new file mode 100644
index 0000000000000..98dad3a0b169b
--- /dev/null
+++ b/src/fetchers/wakatime-fetcher.js
@@ -0,0 +1,13 @@
+const axios = require("axios");
+
+const fetchLast7Days = async ({ username }) => {
+ const { data } = await axios.get(
+ `https://wakatime.com/api/v1/users/${username}/stats/last_7_days?is_including_today=true`
+ );
+
+ return data.data;
+};
+
+module.exports = {
+ fetchLast7Days,
+};
diff --git a/tests/fetchWakatime.test.js b/tests/fetchWakatime.test.js
new file mode 100644
index 0000000000000..29aaa010b32d4
--- /dev/null
+++ b/tests/fetchWakatime.test.js
@@ -0,0 +1,215 @@
+require("@testing-library/jest-dom");
+const axios = require("axios");
+const MockAdapter = require("axios-mock-adapter");
+const { fetchLast7Days } = require("../src/fetchers/wakatime-fetcher");
+const mock = new MockAdapter(axios);
+
+afterEach(() => {
+ mock.reset();
+});
+
+const wakaTimeData = {
+ data: {
+ categories: [
+ {
+ digital: "22:40",
+ hours: 22,
+ minutes: 40,
+ name: "Coding",
+ percent: 100,
+ text: "22 hrs 40 mins",
+ total_seconds: 81643.570077,
+ },
+ ],
+ daily_average: 16095,
+ daily_average_including_other_language: 16329,
+ days_including_holidays: 7,
+ days_minus_holidays: 5,
+ editors: [
+ {
+ digital: "22:40",
+ hours: 22,
+ minutes: 40,
+ name: "VS Code",
+ percent: 100,
+ text: "22 hrs 40 mins",
+ total_seconds: 81643.570077,
+ },
+ ],
+ holidays: 2,
+ human_readable_daily_average: "4 hrs 28 mins",
+ human_readable_daily_average_including_other_language: "4 hrs 32 mins",
+ human_readable_total: "22 hrs 21 mins",
+ human_readable_total_including_other_language: "22 hrs 40 mins",
+ id: "random hash",
+ is_already_updating: false,
+ is_coding_activity_visible: true,
+ is_including_today: false,
+ is_other_usage_visible: true,
+ is_stuck: false,
+ is_up_to_date: true,
+ languages: [
+ {
+ digital: "0:19",
+ hours: 0,
+ minutes: 19,
+ name: "Other",
+ percent: 1.43,
+ text: "19 mins",
+ total_seconds: 1170.434361,
+ },
+ {
+ digital: "0:01",
+ hours: 0,
+ minutes: 1,
+ name: "TypeScript",
+ percent: 0.1,
+ text: "1 min",
+ total_seconds: 83.293809,
+ },
+ {
+ digital: "0:00",
+ hours: 0,
+ minutes: 0,
+ name: "YAML",
+ percent: 0.07,
+ text: "0 secs",
+ total_seconds: 54.975151,
+ },
+ ],
+ operating_systems: [
+ {
+ digital: "22:40",
+ hours: 22,
+ minutes: 40,
+ name: "Mac",
+ percent: 100,
+ text: "22 hrs 40 mins",
+ total_seconds: 81643.570077,
+ },
+ ],
+ percent_calculated: 100,
+ range: "last_7_days",
+ status: "ok",
+ timeout: 15,
+ total_seconds: 80473.135716,
+ total_seconds_including_other_language: 81643.570077,
+ user_id: "random hash",
+ username: "anuraghazra",
+ writes_only: false,
+ },
+};
+
+describe("Wakatime fetcher", () => {
+ it("should fetch correct wakatime data", async () => {
+ const username = "anuraghazra";
+ mock
+ .onGet(
+ `https://wakatime.com/api/v1/users/${username}/stats/last_7_days?is_including_today=true`
+ )
+ .reply(200, wakaTimeData);
+
+ const repo = await fetchLast7Days({ username });
+ expect(repo).toMatchInlineSnapshot(`
+ Object {
+ "categories": Array [
+ Object {
+ "digital": "22:40",
+ "hours": 22,
+ "minutes": 40,
+ "name": "Coding",
+ "percent": 100,
+ "text": "22 hrs 40 mins",
+ "total_seconds": 81643.570077,
+ },
+ ],
+ "daily_average": 16095,
+ "daily_average_including_other_language": 16329,
+ "days_including_holidays": 7,
+ "days_minus_holidays": 5,
+ "editors": Array [
+ Object {
+ "digital": "22:40",
+ "hours": 22,
+ "minutes": 40,
+ "name": "VS Code",
+ "percent": 100,
+ "text": "22 hrs 40 mins",
+ "total_seconds": 81643.570077,
+ },
+ ],
+ "holidays": 2,
+ "human_readable_daily_average": "4 hrs 28 mins",
+ "human_readable_daily_average_including_other_language": "4 hrs 32 mins",
+ "human_readable_total": "22 hrs 21 mins",
+ "human_readable_total_including_other_language": "22 hrs 40 mins",
+ "id": "random hash",
+ "is_already_updating": false,
+ "is_coding_activity_visible": true,
+ "is_including_today": false,
+ "is_other_usage_visible": true,
+ "is_stuck": false,
+ "is_up_to_date": true,
+ "languages": Array [
+ Object {
+ "digital": "0:19",
+ "hours": 0,
+ "minutes": 19,
+ "name": "Other",
+ "percent": 1.43,
+ "text": "19 mins",
+ "total_seconds": 1170.434361,
+ },
+ Object {
+ "digital": "0:01",
+ "hours": 0,
+ "minutes": 1,
+ "name": "TypeScript",
+ "percent": 0.1,
+ "text": "1 min",
+ "total_seconds": 83.293809,
+ },
+ Object {
+ "digital": "0:00",
+ "hours": 0,
+ "minutes": 0,
+ "name": "YAML",
+ "percent": 0.07,
+ "text": "0 secs",
+ "total_seconds": 54.975151,
+ },
+ ],
+ "operating_systems": Array [
+ Object {
+ "digital": "22:40",
+ "hours": 22,
+ "minutes": 40,
+ "name": "Mac",
+ "percent": 100,
+ "text": "22 hrs 40 mins",
+ "total_seconds": 81643.570077,
+ },
+ ],
+ "percent_calculated": 100,
+ "range": "last_7_days",
+ "status": "ok",
+ "timeout": 15,
+ "total_seconds": 80473.135716,
+ "total_seconds_including_other_language": 81643.570077,
+ "user_id": "random hash",
+ "username": "anuraghazra",
+ "writes_only": false,
+ }
+ `);
+ });
+
+ it("should throw error", async () => {
+ mock.onGet(/\/https:\/\/wakatime\.com\/api/).reply(404, wakaTimeData);
+
+ await expect(fetchLast7Days("noone")).rejects.toThrow(
+ "Request failed with status code 404"
+ );
+ });
+});
+
+module.exports = { wakaTimeData };
diff --git a/tests/renderWakatimeCard.test.js b/tests/renderWakatimeCard.test.js
new file mode 100644
index 0000000000000..6daa30a1cc126
--- /dev/null
+++ b/tests/renderWakatimeCard.test.js
@@ -0,0 +1,116 @@
+require("@testing-library/jest-dom");
+const renderWakatimeCard = require("../src/cards/wakatime-card");
+
+const { wakaTimeData } = require("./fetchWakatime.test");
+
+describe("Test Render Wakatime Card", () => {
+ it("should render correctly", () => {
+ const card = renderWakatimeCard(wakaTimeData);
+
+ expect(card).toMatchInlineSnapshot(`
+ "
+
+ "
+ `);
+ });
+});