mirror of
https://github.com/System-End/github-readme-stats.git
synced 2026-04-19 22:15:15 +00:00
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:
parent
e377770a71
commit
6e73a0035d
7 changed files with 586 additions and 0 deletions
59
api/wakatime.js
Normal file
59
api/wakatime.js
Normal 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));
|
||||
}
|
||||
};
|
||||
|
|
@ -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": {
|
||||
|
|
|
|||
26
readme.md
26
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.
|
|||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
# Wakatime Week Stats
|
||||
|
||||
Change the `?username=` value to your Wakatime username.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
### Demo
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
---
|
||||
|
||||
### All Demos
|
||||
|
|
@ -287,6 +309,10 @@ Choose from any of the [default themes](#themes)
|
|||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
- Wakatime card
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
---
|
||||
|
||||
### Quick Tip (Align The Repo Cards)
|
||||
|
|
|
|||
156
src/cards/wakatime-card.js
Normal file
156
src/cards/wakatime-card.js
Normal 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;
|
||||
13
src/fetchers/wakatime-fetcher.js
Normal file
13
src/fetchers/wakatime-fetcher.js
Normal 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
215
tests/fetchWakatime.test.js
Normal 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 };
|
||||
116
tests/renderWakatimeCard.test.js
Normal file
116
tests/renderWakatimeCard.test.js
Normal 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>
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue