@dytsou/github-readme-stats 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/api/gist.js +98 -0
- package/api/index.js +146 -0
- package/api/pin.js +114 -0
- package/api/status/pat-info.js +193 -0
- package/api/status/up.js +129 -0
- package/api/top-langs.js +163 -0
- package/api/wakatime.js +129 -0
- package/package.json +102 -0
- package/readme.md +412 -0
- package/src/calculateRank.js +87 -0
- package/src/cards/gist.js +151 -0
- package/src/cards/index.js +4 -0
- package/src/cards/repo.js +199 -0
- package/src/cards/stats.js +607 -0
- package/src/cards/top-languages.js +968 -0
- package/src/cards/types.d.ts +67 -0
- package/src/cards/wakatime.js +482 -0
- package/src/common/Card.js +294 -0
- package/src/common/I18n.js +41 -0
- package/src/common/access.js +69 -0
- package/src/common/api-utils.js +221 -0
- package/src/common/blacklist.js +10 -0
- package/src/common/cache.js +153 -0
- package/src/common/color.js +194 -0
- package/src/common/envs.js +15 -0
- package/src/common/error.js +84 -0
- package/src/common/fmt.js +90 -0
- package/src/common/html.js +43 -0
- package/src/common/http.js +24 -0
- package/src/common/icons.js +63 -0
- package/src/common/index.js +13 -0
- package/src/common/languageColors.json +651 -0
- package/src/common/log.js +14 -0
- package/src/common/ops.js +124 -0
- package/src/common/render.js +261 -0
- package/src/common/retryer.js +97 -0
- package/src/common/worker-adapter.js +148 -0
- package/src/common/worker-env.js +48 -0
- package/src/fetchers/gist.js +114 -0
- package/src/fetchers/repo.js +118 -0
- package/src/fetchers/stats.js +350 -0
- package/src/fetchers/top-languages.js +192 -0
- package/src/fetchers/types.d.ts +118 -0
- package/src/fetchers/wakatime.js +109 -0
- package/src/index.js +2 -0
- package/src/translations.js +1105 -0
- package/src/worker.ts +116 -0
- package/themes/README.md +229 -0
- package/themes/index.js +467 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { Card } from "../common/Card.js";
|
|
4
|
+
import { getCardColors } from "../common/color.js";
|
|
5
|
+
import { CustomError } from "../common/error.js";
|
|
6
|
+
import { kFormatter } from "../common/fmt.js";
|
|
7
|
+
import { I18n } from "../common/I18n.js";
|
|
8
|
+
import { icons, rankIcon } from "../common/icons.js";
|
|
9
|
+
import { escapeCSSValue } from "../common/html.js";
|
|
10
|
+
import { clampValue } from "../common/ops.js";
|
|
11
|
+
import { flexLayout, measureText } from "../common/render.js";
|
|
12
|
+
import { statCardLocales, wakatimeCardLocales } from "../translations.js";
|
|
13
|
+
|
|
14
|
+
const CARD_MIN_WIDTH = 287;
|
|
15
|
+
const CARD_DEFAULT_WIDTH = 287;
|
|
16
|
+
const RANK_CARD_MIN_WIDTH = 420;
|
|
17
|
+
const RANK_CARD_DEFAULT_WIDTH = 450;
|
|
18
|
+
const RANK_ONLY_CARD_MIN_WIDTH = 290;
|
|
19
|
+
const RANK_ONLY_CARD_DEFAULT_WIDTH = 290;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Long locales that need more space for text. Keep sorted alphabetically.
|
|
23
|
+
*
|
|
24
|
+
* @type {(keyof typeof wakatimeCardLocales["wakatimecard.title"])[]}
|
|
25
|
+
*/
|
|
26
|
+
const LONG_LOCALES = [
|
|
27
|
+
"az",
|
|
28
|
+
"bg",
|
|
29
|
+
"cs",
|
|
30
|
+
"de",
|
|
31
|
+
"el",
|
|
32
|
+
"es",
|
|
33
|
+
"fil",
|
|
34
|
+
"fi",
|
|
35
|
+
"fr",
|
|
36
|
+
"hu",
|
|
37
|
+
"id",
|
|
38
|
+
"ja",
|
|
39
|
+
"ml",
|
|
40
|
+
"my",
|
|
41
|
+
"nl",
|
|
42
|
+
"pl",
|
|
43
|
+
"pt-br",
|
|
44
|
+
"pt-pt",
|
|
45
|
+
"ru",
|
|
46
|
+
"sr",
|
|
47
|
+
"sr-latn",
|
|
48
|
+
"sw",
|
|
49
|
+
"ta",
|
|
50
|
+
"uk-ua",
|
|
51
|
+
"uz",
|
|
52
|
+
"zh-tw",
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a stats card text item.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} params Object that contains the createTextNode parameters.
|
|
59
|
+
* @param {string} params.icon The icon to display.
|
|
60
|
+
* @param {string} params.label The label to display.
|
|
61
|
+
* @param {number} params.value The value to display.
|
|
62
|
+
* @param {string} params.id The id of the stat.
|
|
63
|
+
* @param {string=} params.unitSymbol The unit symbol of the stat.
|
|
64
|
+
* @param {number} params.index The index of the stat.
|
|
65
|
+
* @param {boolean} params.showIcons Whether to show icons.
|
|
66
|
+
* @param {number} params.shiftValuePos Number of pixels the value has to be shifted to the right.
|
|
67
|
+
* @param {boolean} params.bold Whether to bold the label.
|
|
68
|
+
* @param {string} params.numberFormat The format of numbers on card.
|
|
69
|
+
* @param {number=} params.numberPrecision The precision of numbers on card.
|
|
70
|
+
* @returns {string} The stats card text item SVG object.
|
|
71
|
+
*/
|
|
72
|
+
const createTextNode = ({
|
|
73
|
+
icon,
|
|
74
|
+
label,
|
|
75
|
+
value,
|
|
76
|
+
id,
|
|
77
|
+
unitSymbol,
|
|
78
|
+
index,
|
|
79
|
+
showIcons,
|
|
80
|
+
shiftValuePos,
|
|
81
|
+
bold,
|
|
82
|
+
numberFormat,
|
|
83
|
+
numberPrecision,
|
|
84
|
+
}) => {
|
|
85
|
+
const precision =
|
|
86
|
+
typeof numberPrecision === "number" && !isNaN(numberPrecision)
|
|
87
|
+
? clampValue(numberPrecision, 0, 2)
|
|
88
|
+
: undefined;
|
|
89
|
+
const kValue =
|
|
90
|
+
numberFormat.toLowerCase() === "long" || id === "prs_merged_percentage"
|
|
91
|
+
? value
|
|
92
|
+
: kFormatter(value, precision);
|
|
93
|
+
const staggerDelay = (index + 3) * 150;
|
|
94
|
+
|
|
95
|
+
const labelOffset = showIcons ? `x="25"` : "";
|
|
96
|
+
const iconSvg = showIcons
|
|
97
|
+
? `
|
|
98
|
+
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
|
99
|
+
${icon}
|
|
100
|
+
</svg>
|
|
101
|
+
`
|
|
102
|
+
: "";
|
|
103
|
+
return `
|
|
104
|
+
<g class="stagger" style="animation-delay: ${staggerDelay}ms" transform="translate(25, 0)">
|
|
105
|
+
${iconSvg}
|
|
106
|
+
<text class="stat ${
|
|
107
|
+
bold ? " bold" : "not_bold"
|
|
108
|
+
}" ${labelOffset} y="12.5">${label}:</text>
|
|
109
|
+
<text
|
|
110
|
+
class="stat ${bold ? " bold" : "not_bold"}"
|
|
111
|
+
x="${(showIcons ? 140 : 120) + shiftValuePos}"
|
|
112
|
+
y="12.5"
|
|
113
|
+
data-testid="${id}"
|
|
114
|
+
>${kValue}${unitSymbol ? ` ${unitSymbol}` : ""}</text>
|
|
115
|
+
</g>
|
|
116
|
+
`;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Calculates progress along the boundary of the circle, i.e. its circumference.
|
|
121
|
+
*
|
|
122
|
+
* @param {number} value The rank value to calculate progress for.
|
|
123
|
+
* @returns {number} Progress value.
|
|
124
|
+
*/
|
|
125
|
+
const calculateCircleProgress = (value) => {
|
|
126
|
+
const radius = 40;
|
|
127
|
+
const c = Math.PI * (radius * 2);
|
|
128
|
+
|
|
129
|
+
if (value < 0) {
|
|
130
|
+
value = 0;
|
|
131
|
+
}
|
|
132
|
+
if (value > 100) {
|
|
133
|
+
value = 100;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return ((100 - value) / 100) * c;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Retrieves the animation to display progress along the circumference of circle
|
|
141
|
+
* from the beginning to the given value in a clockwise direction.
|
|
142
|
+
*
|
|
143
|
+
* @param {{progress: number}} progress The progress value to animate to.
|
|
144
|
+
* @returns {string} Progress animation css.
|
|
145
|
+
*/
|
|
146
|
+
const getProgressAnimation = ({ progress }) => {
|
|
147
|
+
return `
|
|
148
|
+
@keyframes rankAnimation {
|
|
149
|
+
from {
|
|
150
|
+
stroke-dashoffset: ${calculateCircleProgress(0)};
|
|
151
|
+
}
|
|
152
|
+
to {
|
|
153
|
+
stroke-dashoffset: ${calculateCircleProgress(progress)};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
`;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Retrieves CSS styles for a card.
|
|
161
|
+
*
|
|
162
|
+
* @param {Object} colors The colors to use for the card.
|
|
163
|
+
* @param {string} colors.titleColor The title color.
|
|
164
|
+
* @param {string} colors.textColor The text color.
|
|
165
|
+
* @param {string} colors.iconColor The icon color.
|
|
166
|
+
* @param {string} colors.ringColor The ring color.
|
|
167
|
+
* @param {boolean} colors.show_icons Whether to show icons.
|
|
168
|
+
* @param {number} colors.progress The progress value to animate to.
|
|
169
|
+
* @returns {string} Card CSS styles.
|
|
170
|
+
*/
|
|
171
|
+
const getStyles = ({
|
|
172
|
+
// eslint-disable-next-line no-unused-vars
|
|
173
|
+
titleColor,
|
|
174
|
+
textColor,
|
|
175
|
+
iconColor,
|
|
176
|
+
ringColor,
|
|
177
|
+
show_icons,
|
|
178
|
+
progress,
|
|
179
|
+
}) => {
|
|
180
|
+
// Sanitize color values to prevent XSS
|
|
181
|
+
const safeTextColor = escapeCSSValue(textColor);
|
|
182
|
+
const safeIconColor = escapeCSSValue(iconColor);
|
|
183
|
+
const safeRingColor = escapeCSSValue(ringColor);
|
|
184
|
+
return `
|
|
185
|
+
.stat {
|
|
186
|
+
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${safeTextColor};
|
|
187
|
+
}
|
|
188
|
+
@supports(-moz-appearance: auto) {
|
|
189
|
+
/* Selector detects Firefox */
|
|
190
|
+
.stat { font-size:12px; }
|
|
191
|
+
}
|
|
192
|
+
.stagger {
|
|
193
|
+
opacity: 0;
|
|
194
|
+
animation: fadeInAnimation 0.3s ease-in-out forwards;
|
|
195
|
+
}
|
|
196
|
+
.rank-text {
|
|
197
|
+
font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${safeTextColor};
|
|
198
|
+
animation: scaleInAnimation 0.3s ease-in-out forwards;
|
|
199
|
+
}
|
|
200
|
+
.rank-percentile-header {
|
|
201
|
+
font-size: 14px;
|
|
202
|
+
}
|
|
203
|
+
.rank-percentile-text {
|
|
204
|
+
font-size: 16px;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.not_bold { font-weight: 400 }
|
|
208
|
+
.bold { font-weight: 700 }
|
|
209
|
+
.icon {
|
|
210
|
+
fill: ${safeIconColor};
|
|
211
|
+
display: ${show_icons ? "block" : "none"};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.rank-circle-rim {
|
|
215
|
+
stroke: ${safeRingColor};
|
|
216
|
+
fill: none;
|
|
217
|
+
stroke-width: 6;
|
|
218
|
+
opacity: 0.2;
|
|
219
|
+
}
|
|
220
|
+
.rank-circle {
|
|
221
|
+
stroke: ${safeRingColor};
|
|
222
|
+
stroke-dasharray: 250;
|
|
223
|
+
fill: none;
|
|
224
|
+
stroke-width: 6;
|
|
225
|
+
stroke-linecap: round;
|
|
226
|
+
opacity: 0.8;
|
|
227
|
+
transform-origin: -10px 8px;
|
|
228
|
+
transform: rotate(-90deg);
|
|
229
|
+
animation: rankAnimation 1s forwards ease-in-out;
|
|
230
|
+
}
|
|
231
|
+
${process.env.NODE_ENV === "test" ? "" : getProgressAnimation({ progress })}
|
|
232
|
+
`;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Return the label for commits according to the selected options
|
|
237
|
+
*
|
|
238
|
+
* @param {boolean} include_all_commits Option to include all years
|
|
239
|
+
* @param {number|undefined} commits_year Option to include only selected year
|
|
240
|
+
* @param {I18n} i18n The I18n instance.
|
|
241
|
+
* @returns {string} The label corresponding to the options.
|
|
242
|
+
*/
|
|
243
|
+
const getTotalCommitsYearLabel = (include_all_commits, commits_year, i18n) =>
|
|
244
|
+
include_all_commits
|
|
245
|
+
? ""
|
|
246
|
+
: commits_year
|
|
247
|
+
? ` (${commits_year})`
|
|
248
|
+
: ` (${i18n.t("wakatimecard.lastyear")})`;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @typedef {import('../fetchers/types').StatsData} StatsData
|
|
252
|
+
* @typedef {import('./types').StatCardOptions} StatCardOptions
|
|
253
|
+
*/
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Renders the stats card.
|
|
257
|
+
*
|
|
258
|
+
* @param {StatsData} stats The stats data.
|
|
259
|
+
* @param {Partial<StatCardOptions>} options The card options.
|
|
260
|
+
* @returns {string} The stats card SVG object.
|
|
261
|
+
*/
|
|
262
|
+
const renderStatsCard = (stats, options = {}) => {
|
|
263
|
+
const {
|
|
264
|
+
name,
|
|
265
|
+
totalStars,
|
|
266
|
+
totalCommits,
|
|
267
|
+
totalIssues,
|
|
268
|
+
totalPRs,
|
|
269
|
+
totalPRsMerged,
|
|
270
|
+
mergedPRsPercentage,
|
|
271
|
+
totalReviews,
|
|
272
|
+
totalDiscussionsStarted,
|
|
273
|
+
totalDiscussionsAnswered,
|
|
274
|
+
contributedTo,
|
|
275
|
+
rank,
|
|
276
|
+
} = stats;
|
|
277
|
+
const {
|
|
278
|
+
hide = [],
|
|
279
|
+
show_icons = false,
|
|
280
|
+
hide_title = false,
|
|
281
|
+
hide_border = false,
|
|
282
|
+
card_width,
|
|
283
|
+
hide_rank = false,
|
|
284
|
+
include_all_commits = false,
|
|
285
|
+
commits_year,
|
|
286
|
+
line_height = 25,
|
|
287
|
+
title_color,
|
|
288
|
+
ring_color,
|
|
289
|
+
icon_color,
|
|
290
|
+
text_color,
|
|
291
|
+
text_bold = true,
|
|
292
|
+
bg_color,
|
|
293
|
+
theme = "default",
|
|
294
|
+
custom_title,
|
|
295
|
+
border_radius,
|
|
296
|
+
border_color,
|
|
297
|
+
number_format = "short",
|
|
298
|
+
number_precision,
|
|
299
|
+
locale,
|
|
300
|
+
disable_animations = false,
|
|
301
|
+
rank_icon = "default",
|
|
302
|
+
show = [],
|
|
303
|
+
} = options;
|
|
304
|
+
|
|
305
|
+
const lheight = parseInt(String(line_height), 10);
|
|
306
|
+
|
|
307
|
+
// returns theme based colors with proper overrides and defaults
|
|
308
|
+
const { titleColor, iconColor, textColor, bgColor, borderColor, ringColor } =
|
|
309
|
+
getCardColors({
|
|
310
|
+
title_color,
|
|
311
|
+
text_color,
|
|
312
|
+
icon_color,
|
|
313
|
+
bg_color,
|
|
314
|
+
border_color,
|
|
315
|
+
ring_color,
|
|
316
|
+
theme,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const apostrophe = /s$/i.test(name.trim()) ? "" : "s";
|
|
320
|
+
const i18n = new I18n({
|
|
321
|
+
locale,
|
|
322
|
+
translations: {
|
|
323
|
+
...statCardLocales({ name, apostrophe }),
|
|
324
|
+
...wakatimeCardLocales,
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Meta data for creating text nodes with createTextNode function
|
|
329
|
+
const STATS = {};
|
|
330
|
+
|
|
331
|
+
STATS.stars = {
|
|
332
|
+
icon: icons.star,
|
|
333
|
+
label: i18n.t("statcard.totalstars"),
|
|
334
|
+
value: totalStars,
|
|
335
|
+
id: "stars",
|
|
336
|
+
};
|
|
337
|
+
STATS.commits = {
|
|
338
|
+
icon: icons.commits,
|
|
339
|
+
label: `${i18n.t("statcard.commits")}${getTotalCommitsYearLabel(
|
|
340
|
+
include_all_commits,
|
|
341
|
+
commits_year,
|
|
342
|
+
i18n,
|
|
343
|
+
)}`,
|
|
344
|
+
value: totalCommits,
|
|
345
|
+
id: "commits",
|
|
346
|
+
};
|
|
347
|
+
STATS.prs = {
|
|
348
|
+
icon: icons.prs,
|
|
349
|
+
label: i18n.t("statcard.prs"),
|
|
350
|
+
value: totalPRs,
|
|
351
|
+
id: "prs",
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
if (show.includes("prs_merged")) {
|
|
355
|
+
STATS.prs_merged = {
|
|
356
|
+
icon: icons.prs_merged,
|
|
357
|
+
label: i18n.t("statcard.prs-merged"),
|
|
358
|
+
value: totalPRsMerged,
|
|
359
|
+
id: "prs_merged",
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (show.includes("prs_merged_percentage")) {
|
|
364
|
+
STATS.prs_merged_percentage = {
|
|
365
|
+
icon: icons.prs_merged_percentage,
|
|
366
|
+
label: i18n.t("statcard.prs-merged-percentage"),
|
|
367
|
+
value: mergedPRsPercentage.toFixed(
|
|
368
|
+
typeof number_precision === "number" && !isNaN(number_precision)
|
|
369
|
+
? clampValue(number_precision, 0, 2)
|
|
370
|
+
: 2,
|
|
371
|
+
),
|
|
372
|
+
id: "prs_merged_percentage",
|
|
373
|
+
unitSymbol: "%",
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (show.includes("reviews")) {
|
|
378
|
+
STATS.reviews = {
|
|
379
|
+
icon: icons.reviews,
|
|
380
|
+
label: i18n.t("statcard.reviews"),
|
|
381
|
+
value: totalReviews,
|
|
382
|
+
id: "reviews",
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
STATS.issues = {
|
|
387
|
+
icon: icons.issues,
|
|
388
|
+
label: i18n.t("statcard.issues"),
|
|
389
|
+
value: totalIssues,
|
|
390
|
+
id: "issues",
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
if (show.includes("discussions_started")) {
|
|
394
|
+
STATS.discussions_started = {
|
|
395
|
+
icon: icons.discussions_started,
|
|
396
|
+
label: i18n.t("statcard.discussions-started"),
|
|
397
|
+
value: totalDiscussionsStarted,
|
|
398
|
+
id: "discussions_started",
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
if (show.includes("discussions_answered")) {
|
|
402
|
+
STATS.discussions_answered = {
|
|
403
|
+
icon: icons.discussions_answered,
|
|
404
|
+
label: i18n.t("statcard.discussions-answered"),
|
|
405
|
+
value: totalDiscussionsAnswered,
|
|
406
|
+
id: "discussions_answered",
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
STATS.contribs = {
|
|
411
|
+
icon: icons.contribs,
|
|
412
|
+
label: i18n.t("statcard.contribs"),
|
|
413
|
+
value: contributedTo,
|
|
414
|
+
id: "contribs",
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// @ts-ignore
|
|
418
|
+
const isLongLocale = locale ? LONG_LOCALES.includes(locale) : false;
|
|
419
|
+
|
|
420
|
+
// filter out hidden stats defined by user & create the text nodes
|
|
421
|
+
const statItems = Object.keys(STATS)
|
|
422
|
+
.filter((key) => !hide.includes(key))
|
|
423
|
+
.map((key, index) => {
|
|
424
|
+
// @ts-ignore
|
|
425
|
+
const stats = STATS[key];
|
|
426
|
+
|
|
427
|
+
// create the text nodes, and pass index so that we can calculate the line spacing
|
|
428
|
+
return createTextNode({
|
|
429
|
+
icon: stats.icon,
|
|
430
|
+
label: stats.label,
|
|
431
|
+
value: stats.value,
|
|
432
|
+
id: stats.id,
|
|
433
|
+
unitSymbol: stats.unitSymbol,
|
|
434
|
+
index,
|
|
435
|
+
showIcons: show_icons,
|
|
436
|
+
shiftValuePos: 79.01 + (isLongLocale ? 50 : 0),
|
|
437
|
+
bold: text_bold,
|
|
438
|
+
numberFormat: number_format,
|
|
439
|
+
numberPrecision: number_precision,
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
if (statItems.length === 0 && hide_rank) {
|
|
444
|
+
throw new CustomError(
|
|
445
|
+
"Could not render stats card.",
|
|
446
|
+
"Either stats or rank are required.",
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Calculate the card height depending on how many items there are
|
|
451
|
+
// but if rank circle is visible clamp the minimum height to `150`
|
|
452
|
+
let height = Math.max(
|
|
453
|
+
45 + (statItems.length + 1) * lheight,
|
|
454
|
+
hide_rank ? 0 : statItems.length ? 150 : 180,
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
// the lower the user's percentile the better
|
|
458
|
+
const progress = 100 - rank.percentile;
|
|
459
|
+
const cssStyles = getStyles({
|
|
460
|
+
titleColor,
|
|
461
|
+
ringColor,
|
|
462
|
+
textColor,
|
|
463
|
+
iconColor,
|
|
464
|
+
show_icons,
|
|
465
|
+
progress,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
const calculateTextWidth = () => {
|
|
469
|
+
return measureText(
|
|
470
|
+
custom_title
|
|
471
|
+
? custom_title
|
|
472
|
+
: statItems.length
|
|
473
|
+
? i18n.t("statcard.title")
|
|
474
|
+
: i18n.t("statcard.ranktitle"),
|
|
475
|
+
);
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
/*
|
|
479
|
+
When hide_rank=true, the minimum card width is 270 px + the title length and padding.
|
|
480
|
+
When hide_rank=false, the minimum card_width is 340 px + the icon width (if show_icons=true).
|
|
481
|
+
Numbers are picked by looking at existing dimensions on production.
|
|
482
|
+
*/
|
|
483
|
+
const iconWidth = show_icons && statItems.length ? 16 + /* padding */ 1 : 0;
|
|
484
|
+
const minCardWidth =
|
|
485
|
+
(hide_rank
|
|
486
|
+
? clampValue(
|
|
487
|
+
50 /* padding */ + calculateTextWidth() * 2,
|
|
488
|
+
CARD_MIN_WIDTH,
|
|
489
|
+
Infinity,
|
|
490
|
+
)
|
|
491
|
+
: statItems.length
|
|
492
|
+
? RANK_CARD_MIN_WIDTH
|
|
493
|
+
: RANK_ONLY_CARD_MIN_WIDTH) + iconWidth;
|
|
494
|
+
const defaultCardWidth =
|
|
495
|
+
(hide_rank
|
|
496
|
+
? CARD_DEFAULT_WIDTH
|
|
497
|
+
: statItems.length
|
|
498
|
+
? RANK_CARD_DEFAULT_WIDTH
|
|
499
|
+
: RANK_ONLY_CARD_DEFAULT_WIDTH) + iconWidth;
|
|
500
|
+
let width = card_width
|
|
501
|
+
? isNaN(card_width)
|
|
502
|
+
? defaultCardWidth
|
|
503
|
+
: card_width
|
|
504
|
+
: defaultCardWidth;
|
|
505
|
+
if (width < minCardWidth) {
|
|
506
|
+
width = minCardWidth;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const card = new Card({
|
|
510
|
+
customTitle: custom_title,
|
|
511
|
+
defaultTitle: statItems.length
|
|
512
|
+
? i18n.t("statcard.title")
|
|
513
|
+
: i18n.t("statcard.ranktitle"),
|
|
514
|
+
width,
|
|
515
|
+
height,
|
|
516
|
+
border_radius,
|
|
517
|
+
colors: {
|
|
518
|
+
titleColor,
|
|
519
|
+
textColor,
|
|
520
|
+
iconColor,
|
|
521
|
+
bgColor,
|
|
522
|
+
borderColor,
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
card.setHideBorder(hide_border);
|
|
527
|
+
card.setHideTitle(hide_title);
|
|
528
|
+
card.setCSS(cssStyles);
|
|
529
|
+
|
|
530
|
+
if (disable_animations) {
|
|
531
|
+
card.disableAnimations();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Calculates the right rank circle translation values such that the rank circle
|
|
536
|
+
* keeps respecting the following padding:
|
|
537
|
+
*
|
|
538
|
+
* width > RANK_CARD_DEFAULT_WIDTH: The default right padding of 70 px will be used.
|
|
539
|
+
* width < RANK_CARD_DEFAULT_WIDTH: The left and right padding will be enlarged
|
|
540
|
+
* equally from a certain minimum at RANK_CARD_MIN_WIDTH.
|
|
541
|
+
*
|
|
542
|
+
* @returns {number} - Rank circle translation value.
|
|
543
|
+
*/
|
|
544
|
+
const calculateRankXTranslation = () => {
|
|
545
|
+
if (statItems.length) {
|
|
546
|
+
const minXTranslation = RANK_CARD_MIN_WIDTH + iconWidth - 70;
|
|
547
|
+
if (width > RANK_CARD_DEFAULT_WIDTH) {
|
|
548
|
+
const xMaxExpansion = minXTranslation + (450 - minCardWidth) / 2;
|
|
549
|
+
return xMaxExpansion + width - RANK_CARD_DEFAULT_WIDTH;
|
|
550
|
+
} else {
|
|
551
|
+
return minXTranslation + (width - minCardWidth) / 2;
|
|
552
|
+
}
|
|
553
|
+
} else {
|
|
554
|
+
return width / 2 + 20 - 10;
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
// Conditionally rendered elements
|
|
559
|
+
const rankCircle = hide_rank
|
|
560
|
+
? ""
|
|
561
|
+
: `<g data-testid="rank-circle"
|
|
562
|
+
transform="translate(${calculateRankXTranslation()}, ${
|
|
563
|
+
height / 2 - 50
|
|
564
|
+
})">
|
|
565
|
+
<circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
|
|
566
|
+
<circle class="rank-circle" cx="-10" cy="8" r="40" />
|
|
567
|
+
<g class="rank-text">
|
|
568
|
+
${rankIcon(rank_icon, rank?.level, rank?.percentile)}
|
|
569
|
+
</g>
|
|
570
|
+
</g>`;
|
|
571
|
+
|
|
572
|
+
// Accessibility Labels
|
|
573
|
+
const labels = Object.keys(STATS)
|
|
574
|
+
.filter((key) => !hide.includes(key))
|
|
575
|
+
.map((key) => {
|
|
576
|
+
// @ts-ignore
|
|
577
|
+
const stats = STATS[key];
|
|
578
|
+
if (key === "commits") {
|
|
579
|
+
return `${i18n.t("statcard.commits")} ${getTotalCommitsYearLabel(
|
|
580
|
+
include_all_commits,
|
|
581
|
+
commits_year,
|
|
582
|
+
i18n,
|
|
583
|
+
)} : ${stats.value}`;
|
|
584
|
+
}
|
|
585
|
+
return `${stats.label}: ${stats.value}`;
|
|
586
|
+
})
|
|
587
|
+
.join(", ");
|
|
588
|
+
|
|
589
|
+
card.setAccessibilityLabel({
|
|
590
|
+
title: `${card.title}, Rank: ${rank.level}`,
|
|
591
|
+
desc: labels,
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
return card.render(`
|
|
595
|
+
${rankCircle}
|
|
596
|
+
<svg x="0" y="0">
|
|
597
|
+
${flexLayout({
|
|
598
|
+
items: statItems,
|
|
599
|
+
gap: lheight,
|
|
600
|
+
direction: "column",
|
|
601
|
+
}).join("")}
|
|
602
|
+
</svg>
|
|
603
|
+
`);
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
export { renderStatsCard };
|
|
607
|
+
export default renderStatsCard;
|