feat: add wakatime card (#392)

* Adds wakatime card route

* Adds language progress on wakatime card

* Adds wakatime card on README

* Adds no coding activity node

* Remove percent displayed on wakatime's card

* Update readme

* refactor: refactored code & added tests

Co-authored-by: Anurag <hazru.anurag@gmail.com>
This commit is contained in:
Willian Rodrigues 2020-09-19 04:23:39 -03:00 committed by GitHub
parent e377770a71
commit 6e73a0035d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 586 additions and 0 deletions

59
api/wakatime.js Normal file
View file

@ -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));
}
};

View file

@ -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": {

View file

@ -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)

156
src/cards/wakatime-card.js Normal file
View file

@ -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 `
<text x="25" y="11" class="stat bold" fill="${color}">No coding activity this week</text>
`;
};
const createProgressNode = ({
width,
color,
progress,
progressBarBackgroundColor,
}) => {
const progressPercentage = clampValue(progress, 2, 100);
return `
<svg width="${width}" overflow="auto">
<rect rx="5" ry="5" x="110" y="4" width="${width}" height="8" fill="${progressBarBackgroundColor}"></rect>
<rect
height="8"
fill="${color}"
rx="5" ry="5" x="110" y="4"
data-testid="lang-progress"
width="${progressPercentage}%"
>
</rect>
</svg>
`;
};
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 `
<g class="stagger" style="animation-delay: ${staggerDelay}ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5">${label}:</text>
<text
class="stat"
x="${hideProgress ? 170 : 350}"
y="12.5"
data-testid="${id}"
>${value}</text>
${cardProgress}
</g>
`;
};
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(`
<svg x="0" y="0" width="100%">
${FlexLayout({
items: statItems.length
? statItems
: [noCodingActivityNode({ color: textColor })],
gap: lheight,
direction: "column",
}).join("")}
</svg>
`);
};
module.exports = renderWakatimeCard;

View file

@ -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,
};

215
tests/fetchWakatime.test.js Normal file
View file

@ -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 };

View file

@ -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(`
"
<svg
width=\\"495\\"
height=\\"150\\"
viewBox=\\"0 0 495 150\\"
fill=\\"none\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
>
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: #2f80ed;
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
.stat {
font: 600 14px 'Segoe UI', Ubuntu, \\"Helvetica Neue\\", Sans-Serif; fill: #333;
}
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
.rank-text {
font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: #333;
animation: scaleInAnimation 0.3s ease-in-out forwards;
}
.bold { font-weight: 700 }
.icon {
fill: #4c71f2;
display: none;
}
.rank-circle-rim {
stroke: #2f80ed;
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: #2f80ed;
stroke-dasharray: 250;
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
.lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: #333 }
</style>
undefined
<rect
data-testid=\\"card-bg\\"
x=\\"0.5\\"
y=\\"0.5\\"
rx=\\"4.5\\"
height=\\"99%\\"
stroke=\\"#E4E2E2\\"
width=\\"494\\"
fill=\\"#fffefe\\"
stroke-opacity=\\"1\\"
/>
<g
data-testid=\\"card-title\\"
transform=\\"translate(25, 35)\\"
>
<g transform=\\"translate(0, 0)\\">
<text
x=\\"0\\"
y=\\"0\\"
class=\\"header\\"
data-testid=\\"header\\"
>Wakatime week stats</text>
</g>
</g>
<g
data-testid=\\"main-card-body\\"
transform=\\"translate(0, 55)\\"
>
<svg x=\\"0\\" y=\\"0\\" width=\\"100%\\">
<g transform=\\"translate(0, 0)\\">
<text x=\\"25\\" y=\\"11\\" class=\\"stat bold\\" fill=\\"#333\\">No coding activity this week</text>
</g>
</svg>
</g>
</svg>
"
`);
});
});