forked from mirrored/github-readme-stats
Compare commits
59
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1da12ad80 | ||
|
|
6c79c2d82d | ||
|
|
4f1fa61b25 | ||
|
|
ba7c2f8b55 | ||
|
|
888663a477 | ||
|
|
112000667c | ||
|
|
7aa502d453 | ||
|
|
077d40561a | ||
|
|
99d9d3cde0 | ||
|
|
cd5cbcdb09 | ||
|
|
b2bf4fa487 | ||
|
|
4b17300126 | ||
|
|
a17fa1cf5d | ||
|
|
8667ea9908 | ||
|
|
60fae292a3 | ||
|
|
c1dc7b850c | ||
|
|
4ff2c2a425 | ||
|
|
fbb49e3c80 | ||
|
|
eab1402417 | ||
|
|
cfa84232e2 | ||
|
|
d5d1f44e1c | ||
|
|
8bc69e761f | ||
|
|
06a2a78cfc | ||
|
|
ad9db67b4c | ||
|
|
ab9c563ec5 | ||
|
|
0ff426d258 | ||
|
|
4b8198fa21 | ||
|
|
70f0264905 | ||
|
|
0850a97630 | ||
|
|
5ae959b57c | ||
|
|
77dcdab42c | ||
|
|
227711c628 | ||
|
|
42fdff624f | ||
|
|
9eb0c3cbb4 | ||
|
|
ad3726ee49 | ||
|
|
0efb982c2a | ||
|
|
ada9cf4e0e | ||
|
|
3cb205c65b | ||
|
|
f07cd133d3 | ||
|
|
42a4b6f60a | ||
|
|
5df25a6472 | ||
|
|
4b656ebabb | ||
|
|
b2e34ac8db | ||
|
|
0e02523459 | ||
|
|
9aa39dfe81 | ||
|
|
0c829e9488 | ||
|
|
26cde6bfe9 | ||
|
|
1a4e5a6955 | ||
|
|
56aa307094 | ||
|
|
f9e95b166d | ||
|
|
c0d7493c3f | ||
|
|
f993f21379 | ||
|
|
45f434f083 | ||
|
|
199870aa9f | ||
|
|
8e3147014c | ||
|
|
98f9045f98 | ||
|
|
dcc4ccd18c | ||
|
|
1d10912646 | ||
|
|
bb56e3bb7b |
@@ -11,6 +11,8 @@ body:
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected behaviour
|
||||
|
||||
@@ -9,6 +9,8 @@ body:
|
||||
description:
|
||||
A clear and concise description of what the problem is. Ex. I'm always
|
||||
frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import os
|
||||
|
||||
file = open('./vercel.json', 'r')
|
||||
str = file.read()
|
||||
file = open('./vercel.json', 'w')
|
||||
|
||||
str = str.replace('"maxDuration": 10', '"maxDuration": 30')
|
||||
|
||||
file.write(str)
|
||||
file.close()
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Deployment Prep
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
config:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Deployment Prep
|
||||
run: python ./.github/workflows/deploy-prep.py
|
||||
- uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
branch: vercel
|
||||
create_branch: true
|
||||
push_options: "--force"
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
jobs:
|
||||
e2eTests:
|
||||
if:
|
||||
github.repository == 'anuraghazra/github-readme-stats' &&
|
||||
github.event_name == 'deployment_status' &&
|
||||
github.event.deployment_status.state == 'success'
|
||||
name: Perform 2e2 tests
|
||||
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
closeEmptyIssuesAndTemplates:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
name: Close empty issues
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -4,6 +4,7 @@ on:
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
|
||||
jobs:
|
||||
closeOldThemePrs:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
name: Close stale 'invalid' theme PRs
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
@@ -26,5 +27,5 @@ jobs:
|
||||
|
||||
- run: npm run close-stale-theme-prs
|
||||
env:
|
||||
STALE_DAYS: 15
|
||||
STALE_DAYS: 20
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
|
||||
jobs:
|
||||
showAndLabelTopIssues:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
name: Update top issues Dashboard.
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
name: Update supported languages
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 */30 * *"
|
||||
|
||||
jobs:
|
||||
updateLanguages:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
name: Update supported languages
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Run update-languages-json.js script
|
||||
run: npm run generate-langs-json
|
||||
|
||||
- name: Create Pull Request if upstream language file is changed
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
commit-message: "refactor: update languages JSON"
|
||||
branch: "update_langs/patch"
|
||||
delete-branch: true
|
||||
title: Update languages JSON
|
||||
body:
|
||||
"The
|
||||
[update-langs](https://github.com/anuraghazra/github-readme-stats/actions/workflows/update-langs.yaml)
|
||||
action found new/updated languages in the [upstream languages JSON
|
||||
file](https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml)."
|
||||
labels: "ci, lang-card"
|
||||
+9
-5
@@ -1,4 +1,3 @@
|
||||
import * as dotenv from "dotenv";
|
||||
import { renderStatsCard } from "../src/cards/stats-card.js";
|
||||
import { blacklist } from "../src/common/blacklist.js";
|
||||
import {
|
||||
@@ -11,8 +10,6 @@ import {
|
||||
import { fetchStats } from "../src/fetchers/stats-fetcher.js";
|
||||
import { isLocaleAvailable } from "../src/translations.js";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export default async (req, res) => {
|
||||
const {
|
||||
username,
|
||||
@@ -26,6 +23,7 @@ export default async (req, res) => {
|
||||
include_all_commits,
|
||||
line_height,
|
||||
title_color,
|
||||
ring_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
text_bold,
|
||||
@@ -63,7 +61,12 @@ export default async (req, res) => {
|
||||
CONSTANTS.ONE_DAY,
|
||||
);
|
||||
|
||||
res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`);
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
`max-age=${
|
||||
cacheSeconds / 2
|
||||
}, s-maxage=${cacheSeconds}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
|
||||
);
|
||||
|
||||
return res.send(
|
||||
renderStatsCard(stats, {
|
||||
@@ -76,6 +79,7 @@ export default async (req, res) => {
|
||||
include_all_commits: parseBoolean(include_all_commits),
|
||||
line_height,
|
||||
title_color,
|
||||
ring_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
text_bold: parseBoolean(text_bold),
|
||||
@@ -89,7 +93,7 @@ export default async (req, res) => {
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
res.setHeader("Cache-Control", `no-store`); // Don't cache error responses.
|
||||
res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses.
|
||||
return res.send(renderError(err.message, err.secondaryMessage));
|
||||
}
|
||||
};
|
||||
|
||||
+7
-2
@@ -58,7 +58,12 @@ export default async (req, res) => {
|
||||
cacheSeconds = CONSTANTS.FOUR_HOURS;
|
||||
}
|
||||
|
||||
res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`);
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
`max-age=${
|
||||
cacheSeconds / 2
|
||||
}, s-maxage=${cacheSeconds}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
|
||||
);
|
||||
|
||||
return res.send(
|
||||
renderRepoCard(repoData, {
|
||||
@@ -75,7 +80,7 @@ export default async (req, res) => {
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
res.setHeader("Cache-Control", `no-store`); // Don't cache error responses.
|
||||
res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses.
|
||||
return res.send(renderError(err.message, err.secondaryMessage));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @file Contains a simple cloud function that can be used to check which PATs are no
|
||||
* longer working. It returns a list of valid PATs, expired PATs and PATs with errors.
|
||||
*
|
||||
* @description This function is currently rate limited to 1 request per 10 minutes.
|
||||
*/
|
||||
|
||||
import { logger, request, dateDiff } from "../../src/common/utils.js";
|
||||
export const RATE_LIMIT_SECONDS = 60 * 5; // 1 request per 10 minutes
|
||||
|
||||
/**
|
||||
* Simple uptime check fetcher for the PATs.
|
||||
*
|
||||
* @param {import('axios').AxiosRequestHeaders} variables
|
||||
* @param {string} token
|
||||
*/
|
||||
const uptimeFetcher = (variables, token) => {
|
||||
return request(
|
||||
{
|
||||
query: `
|
||||
query {
|
||||
rateLimit {
|
||||
remaining
|
||||
resetAt
|
||||
},
|
||||
}`,
|
||||
variables,
|
||||
},
|
||||
{
|
||||
Authorization: `bearer ${token}`,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const getAllPATs = () => {
|
||||
return Object.keys(process.env).filter((key) => /PAT_\d*$/.exec(key));
|
||||
};
|
||||
|
||||
/**
|
||||
* Check whether any of the PATs is expired.
|
||||
*/
|
||||
const getPATInfo = async (fetcher, variables) => {
|
||||
const details = {};
|
||||
const PATs = getAllPATs();
|
||||
|
||||
for (const pat of PATs) {
|
||||
try {
|
||||
const response = await fetcher(variables, process.env[pat]);
|
||||
const errors = response.data.errors;
|
||||
const hasErrors = Boolean(errors);
|
||||
const errorType = errors?.[0]?.type;
|
||||
const isRateLimited =
|
||||
(hasErrors && errorType === "RATE_LIMITED") ||
|
||||
response.data.data?.rateLimit?.remaining === 0;
|
||||
|
||||
// Store PATs with errors.
|
||||
if (hasErrors && errorType !== "RATE_LIMITED") {
|
||||
details[pat] = {
|
||||
status: "error",
|
||||
error: {
|
||||
type: errors[0].type,
|
||||
message: errors[0].message,
|
||||
},
|
||||
};
|
||||
continue;
|
||||
} else if (isRateLimited) {
|
||||
const date1 = new Date();
|
||||
const date2 = new Date(response.data?.data?.rateLimit?.resetAt);
|
||||
details[pat] = {
|
||||
status: "exhausted",
|
||||
remaining: 0,
|
||||
resetIn: dateDiff(date2, date1) + " minutes",
|
||||
};
|
||||
} else {
|
||||
details[pat] = {
|
||||
status: "valid",
|
||||
remaining: response.data.data.rateLimit.remaining,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
// Store the PAT if it is expired.
|
||||
const errorMessage = err.response?.data?.message?.toLowerCase();
|
||||
if (errorMessage === "bad credentials") {
|
||||
details[pat] = {
|
||||
status: "expired",
|
||||
};
|
||||
} else if (errorMessage === "sorry. your account was suspended.") {
|
||||
details[pat] = {
|
||||
status: "suspended",
|
||||
};
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const filterPATsByStatus = (status) => {
|
||||
return Object.keys(details).filter((pat) => details[pat].status === status);
|
||||
};
|
||||
|
||||
const sortedDetails = Object.keys(details)
|
||||
.sort()
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = details[key];
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
validPATs: filterPATsByStatus("valid"),
|
||||
expiredPATs: filterPATsByStatus("expired"),
|
||||
exhaustedPATs: filterPATsByStatus("exhausted"),
|
||||
suspendedPATs: filterPATsByStatus("suspended"),
|
||||
errorPATs: filterPATsByStatus("error"),
|
||||
details: sortedDetails,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Cloud function that returns information about the used PATs.
|
||||
*/
|
||||
export default async (_, res) => {
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
try {
|
||||
// Add header to prevent abuse.
|
||||
const PATsInfo = await getPATInfo(uptimeFetcher, {});
|
||||
if (PATsInfo) {
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
`max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`,
|
||||
);
|
||||
}
|
||||
res.send(JSON.stringify(PATsInfo, null, 2));
|
||||
} catch (err) {
|
||||
// Throw error if something went wrong.
|
||||
logger.error(err);
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
res.send("Something went wrong: " + err.message);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* @file Contains a simple cloud function that can be used to check if the PATs are still
|
||||
* functional.
|
||||
*
|
||||
* @description This function is currently rate limited to 1 request per 10 minutes.
|
||||
*/
|
||||
|
||||
import retryer from "../../src/common/retryer.js";
|
||||
import { logger, request } from "../../src/common/utils.js";
|
||||
|
||||
export const RATE_LIMIT_SECONDS = 60 * 10; // 1 request per 10 minutes
|
||||
|
||||
/**
|
||||
* Simple uptime check fetcher for the PATs.
|
||||
*
|
||||
* @param {import('axios').AxiosRequestHeaders} variables
|
||||
* @param {string} token
|
||||
*/
|
||||
const uptimeFetcher = (variables, token) => {
|
||||
return request(
|
||||
{
|
||||
query: `
|
||||
query {
|
||||
rateLimit {
|
||||
remaining
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables,
|
||||
},
|
||||
{
|
||||
Authorization: `bearer ${token}`,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates Json response that can be used for shields.io dynamic card generation.
|
||||
*
|
||||
* @param {*} up Whether the PATs are up or not.
|
||||
* @returns Dynamic shields.io JSON response object.
|
||||
*
|
||||
* @see https://shields.io/endpoint.
|
||||
*/
|
||||
const shieldsUptimeBadge = (up) => {
|
||||
const schemaVersion = 1;
|
||||
const isError = true;
|
||||
const label = "Public Instance";
|
||||
const message = up ? "up" : "down";
|
||||
const color = up ? "brightgreen" : "red";
|
||||
return {
|
||||
schemaVersion,
|
||||
label,
|
||||
message,
|
||||
color,
|
||||
isError,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Cloud function that returns whether the PATs are still functional.
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
let { type } = req.query;
|
||||
type = type ? type.toLowerCase() : "boolean";
|
||||
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
|
||||
try {
|
||||
let PATsValid = true;
|
||||
try {
|
||||
await retryer(uptimeFetcher, {});
|
||||
} catch (err) {
|
||||
PATsValid = false;
|
||||
}
|
||||
|
||||
if (PATsValid) {
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
`max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`,
|
||||
);
|
||||
} else {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case "shields":
|
||||
res.send(shieldsUptimeBadge(PATsValid));
|
||||
break;
|
||||
case "json":
|
||||
res.send({ up: PATsValid });
|
||||
break;
|
||||
default:
|
||||
res.send(PATsValid);
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
// Return fail boolean if something went wrong.
|
||||
logger.error(err);
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
res.send("Something went wrong: " + err.message);
|
||||
}
|
||||
};
|
||||
+11
-5
@@ -1,4 +1,3 @@
|
||||
import * as dotenv from "dotenv";
|
||||
import { renderTopLanguages } from "../src/cards/top-languages-card.js";
|
||||
import { blacklist } from "../src/common/blacklist.js";
|
||||
import {
|
||||
@@ -11,8 +10,6 @@ import {
|
||||
import { fetchTopLanguages } from "../src/fetchers/top-languages-fetcher.js";
|
||||
import { isLocaleAvailable } from "../src/translations.js";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export default async (req, res) => {
|
||||
const {
|
||||
username,
|
||||
@@ -32,6 +29,8 @@ export default async (req, res) => {
|
||||
locale,
|
||||
border_radius,
|
||||
border_color,
|
||||
disable_animations,
|
||||
hide_progress,
|
||||
} = req.query;
|
||||
res.setHeader("Content-Type", "image/svg+xml");
|
||||
|
||||
@@ -55,7 +54,12 @@ export default async (req, res) => {
|
||||
CONSTANTS.ONE_DAY,
|
||||
);
|
||||
|
||||
res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`);
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
`max-age=${
|
||||
cacheSeconds / 2
|
||||
}, s-maxage=${cacheSeconds}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
|
||||
);
|
||||
|
||||
return res.send(
|
||||
renderTopLanguages(topLangs, {
|
||||
@@ -73,10 +77,12 @@ export default async (req, res) => {
|
||||
border_radius,
|
||||
border_color,
|
||||
locale: locale ? locale.toLowerCase() : null,
|
||||
disable_animations: parseBoolean(disable_animations),
|
||||
hide_progress: parseBoolean(hide_progress),
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
res.setHeader("Cache-Control", `no-store`); // Don't cache error responses.
|
||||
res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses.
|
||||
return res.send(renderError(err.message, err.secondaryMessage));
|
||||
}
|
||||
};
|
||||
|
||||
+7
-5
@@ -1,4 +1,3 @@
|
||||
import * as dotenv from "dotenv";
|
||||
import { renderWakatimeCard } from "../src/cards/wakatime-card.js";
|
||||
import {
|
||||
clampValue,
|
||||
@@ -10,8 +9,6 @@ import {
|
||||
import { fetchWakatimeStats } from "../src/fetchers/wakatime-fetcher.js";
|
||||
import { isLocaleAvailable } from "../src/translations.js";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export default async (req, res) => {
|
||||
const {
|
||||
username,
|
||||
@@ -55,7 +52,12 @@ export default async (req, res) => {
|
||||
cacheSeconds = CONSTANTS.FOUR_HOURS;
|
||||
}
|
||||
|
||||
res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`);
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
`max-age=${
|
||||
cacheSeconds / 2
|
||||
}, s-maxage=${cacheSeconds}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
|
||||
);
|
||||
|
||||
return res.send(
|
||||
renderWakatimeCard(stats, {
|
||||
@@ -78,7 +80,7 @@ export default async (req, res) => {
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
res.setHeader("Cache-Control", `no-store`); // Don't cache error responses.
|
||||
res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses.
|
||||
return res.send(renderError(err.message, err.secondaryMessage));
|
||||
}
|
||||
};
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
<p align="center">
|
||||
<img width="100px" src="https://res.cloudinary.com/anuraghazra/image/upload/v1594908242/logo_ccswme.svg" align="center" alt="GitHub Readme Stats" />
|
||||
<h2 align="center">GitHub Readme Stats</h2>
|
||||
<p align="center">동적으로 생성되는 Github 사용량 통계를 여러분의 README 에 추가해보세요!</p>
|
||||
<p align="center">동적으로 생성되는 GitHub 사용량 통계를 여러분의 README 에 추가해보세요!</p>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats/actions">
|
||||
@@ -72,7 +72,7 @@
|
||||
|
||||
아래 코드를 복사해서 마크다운 파일에 붙여넣으면 끝이에요, 아주 간단해요!
|
||||
|
||||
`?username=` 속성의 값을 Github 계정의 사용자 명(닉네임)으로 바꿔주세요.
|
||||
`?username=` 속성의 값을 GitHub 계정의 사용자 명(닉네임)으로 바꿔주세요.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
@@ -235,7 +235,7 @@ GitHub 저장소 여분 핀을 이용하면, 6개 이상의 저장소 핀을 여
|
||||
|
||||
# 언어 사용량 통계
|
||||
|
||||
언어 사용량 통계 카드는 Github 사용자가 가장 많이 사용한 언어가 표시됩니다.
|
||||
언어 사용량 통계 카드는 GitHub 사용자가 가장 많이 사용한 언어가 표시됩니다.
|
||||
|
||||
_참고:
|
||||
언어 사용량 통계는 GitHub 에서 가장 많이 사용된 언어의 표기일 뿐입니다.
|
||||
|
||||
+2
-2
@@ -219,7 +219,7 @@ Use [show_owner](#customization) variable to include the repo's owner username
|
||||
|
||||
टोप भाषाकार्डले github परयोग गर्नेहरुको प्रोग्रम्मिंग भाषाहरु देखाऊने गर्दछ |.
|
||||
|
||||
_NOTE: टोप भाषाहरुले आफ्नो सिपलाए संकेत गरेको होईन | योचै Github Metricबाट धेरै कुन भाषा परयोग भाकोलाए संकेत गरेको हो |
|
||||
_NOTE: टोप भाषाहरुले आफ्नो सिपलाए संकेत गरेको होईन | योचै GitHub Metricबाट धेरै कुन भाषा परयोग भाकोलाए संकेत गरेको हो |
|
||||
### प्रयोग
|
||||
|
||||
कोदलाए कपी- पेसेत readme मा गर्नु होला र लिंक परिवतन गर्नु होला |
|
||||
@@ -389,7 +389,7 @@ NOTE: Since [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) we
|
||||
येदि तपाइले यो प्रोजेक्ट चलाउनु बाकोक्ष बने र मलाई अझै प्रसंसा गर्ने हो बने तपाइले थुप्रै तरिका ले गर्नु सक्नु हुने छ :-
|
||||
|
||||
- यो प्रोजेक्टमा तपाइले प्रहयोग गर्दा मलाई क्रेडिट दिन सक्नु हुनेक्ष ।
|
||||
- तपाइले Github ReadMe Stats स्तार्रेड गर्न सक्नु हुनेक्ष :rocket:
|
||||
- तपाइले GitHub ReadMe Stats स्तार्रेड गर्न सक्नु हुनेक्ष :rocket:
|
||||
- [](https://www.paypal.me/anuraghazra) - तपाइले पेपाल बाट पनि सहयोग (डक्क्षिन) गर्न सक्नु हुनेक्ष | म ~~कोफी ~~ चिया . :tea: किन्न सक्क्षु ।
|
||||
|
||||
धन्याबाद! :heart:
|
||||
|
||||
@@ -53,6 +53,8 @@
|
||||
<a href="/docs/readme_nl.md">Nederlands</a>
|
||||
.
|
||||
<a href="/docs/readme_np.md">नेपाली</a>
|
||||
.
|
||||
<a href="/docs/readme_tr.md">Türkçe</a>
|
||||
</p>
|
||||
</p>
|
||||
<p align="center">Gostou do projeto? Por favor considere <a href="https://www.paypal.me/anuraghazra">fazer uma doação</a> para ajudar a melhorá-lo!
|
||||
@@ -142,7 +144,7 @@ Personalize a aparência do seu `Stats Card` ou `Repo Card` da maneira que desej
|
||||
- `cache_seconds` - Defina o cabeçalho do cache manualmente _(min: 1800, 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). Note também que o cache é limitado a um mínimo de 30 minutos e um máximo de 24 horas.
|
||||
> 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.
|
||||
|
||||
#### Opções exclusivas do cartão de estatísticas:
|
||||
|
||||
@@ -167,7 +169,7 @@ Personalize a aparência do seu `Stats Card` ou `Repo Card` da maneira que desej
|
||||
|
||||
> :warning: **Importante:**
|
||||
> Nomes de linguagens devem ser uma sequência escapada de URI, como específicado em [Codificação por cento](https://pt.wikipedia.org/wiki/Codificação_por_cento)
|
||||
> (Isso é: `c++` deve se tornar `c%2B%2B`, `jupyter notebook` deve se tornar `jupyter%20notebook`, etc.)
|
||||
> (Ou seja: `c++` deve se tornar `c%2B%2B`, `jupyter notebook` deve se tornar `jupyter%20notebook`, etc.)
|
||||
|
||||
---
|
||||
|
||||
|
||||
+7
-7
@@ -73,10 +73,10 @@
|
||||
|
||||
Alt kısımdaki kodu Kopyalayın ve yapıştırın. İşte bu kadar. Çok basit!
|
||||
|
||||
`?username=` değerini kendi Github kullanıcı adınız ile değiştirin.
|
||||
`?username=` değerini kendi GitHub kullanıcı adınız ile değiştirin.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
_Not: Şu sıralamalar mevcut: S+ (en üst 1%), S (en üst 25%), A++ (en üst 45%), A+ (en üst 60%), and B+ (herkes).
|
||||
Buradaki değerler [cumulative distribution function](https://en.wikipedia.org/wiki/Cumulative_distribution_function) ile hesaplanırken; commitler, katkılar, hatalar, yıldızlar, çekme istekleri, takipçiler ve sahip olunan depolar (repository) göz önünde bulundurulamaktadır.
|
||||
@@ -199,7 +199,7 @@ bg_color içerisinde birden fazla rengi gradient olarak göstermek için virgül
|
||||
|
||||
# GitHub Ekstra Pinler
|
||||
|
||||
Github ekstra pinler profilinize 6'dan fazla repoyu / depoyu profilinizde pinleyebilirsiniz.
|
||||
GitHub ekstra pinler profilinize 6'dan fazla repoyu / depoyu profilinizde pinleyebilirsiniz.
|
||||
|
||||
Hey! Artık 6 pin ile kısıtlı kalmayacaksınız!
|
||||
|
||||
@@ -225,7 +225,7 @@ Endpoint: `api/pin?username=mustafacagri&repo=github-readme-stats`
|
||||
|
||||
En çok kullanılan diller kartı kullanıcının en çok kullandığı dilleri gösterir.
|
||||
|
||||
_NOTE: En çok kullanılan dillerde yer alan bilgiler sizin yeteneğinizi ve benzeri şeyleri göstermek. Bu, kodlarınızda en çok kullandığınız dilleri gösteren bir Github metriğidir. Ayrıca, github-readme-stats'ın yeni özelliğidir.
|
||||
_NOTE: En çok kullanılan dillerde yer alan bilgiler sizin yeteneğinizi ve benzeri şeyleri göstermek. Bu, kodlarınızda en çok kullandığınız dilleri gösteren bir GitHub metriğidir. Ayrıca, github-readme-stats'ın yeni özelliğidir.
|
||||
|
||||
### Kullanım
|
||||
|
||||
@@ -365,7 +365,7 @@ Genellikle resimleri yan yana düzenleyemezsiniz. Bunu yapmak için şu yaklaş
|
||||
|
||||
#### [@codeSTACKr'ın Yayınladığı Video Eğitimine Göz Atın](https://youtu.be/n6d4KHSKqGk?t=107)
|
||||
|
||||
Github API saatte sadece 5.000 isteğe izin verdiği için `https://github-readme-stats.vercel.app/api` adresindeki API'm bu limite muhtemelen takılmış olabilir. Eğer projeyi kendi Vercel sunucunuzda yayınlarsanız, böyle bir sorun yaşamayabilirsiniz. Deploy butonuna tıkla ve deploy başlasın!
|
||||
GitHub API saatte sadece 5.000 isteğe izin verdiği için `https://github-readme-stats.vercel.app/api` adresindeki API'm bu limite muhtemelen takılmış olabilir. Eğer projeyi kendi Vercel sunucunuzda yayınlarsanız, böyle bir sorun yaşamayabilirsiniz. Deploy butonuna tıkla ve deploy başlasın!
|
||||
|
||||
|
||||
NOT: [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) geliştirmesi sonrasında anlamadığımız bir şekilde 5.000 istek limitine takılmıyoruz :)
|
||||
@@ -378,9 +378,9 @@ NOT: [#58](https://github.com/anuraghazra/github-readme-stats/pull/58) geliştir
|
||||
1. [vercel.com](https://vercel.com/) adresine gidin
|
||||
1. `Log in`'e tıklayın
|
||||

|
||||
1. `Continue with GitHub`'e basarak Github ile giriş yapın
|
||||
1. `Continue with GitHub`'e basarak GitHub ile giriş yapın
|
||||

|
||||
1. Github'a giriş yapın ve eğer çıkarsa tüm repolara izin verin.
|
||||
1. GitHub'a giriş yapın ve eğer çıkarsa tüm repolara izin verin.
|
||||
1. Bu repoyu fork'layın
|
||||
1. [Vercel dashboard](https://vercel.com/dashboard)'unuza geri dönün.
|
||||
1. `Import Project`'i seçin.
|
||||
|
||||
+15
@@ -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)
|
||||
Generated
+1
-1
@@ -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",
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -85,14 +85,17 @@ Visit <https://indiafightscorona.giveindia.org> and make a small donation to hel
|
||||
- [Top Languages Card](#top-languages-card)
|
||||
- [Wakatime Week Stats](#wakatime-week-stats)
|
||||
- [Themes](#themes)
|
||||
- [Responsive Card Theme](#responsive-card-theme)
|
||||
- [Customization](#customization)
|
||||
- [Common Options](#common-options)
|
||||
- [Stats Card Exclusive Options](#stats-card-exclusive-options)
|
||||
- [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)
|
||||
- [Keep your fork up to date](#keep-your-fork-up-to-date)
|
||||
- [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
|
||||
|
||||
@@ -150,12 +153,101 @@ Use `&theme=THEME_NAME` parameter like so :
|
||||
|
||||
#### All inbuilt themes
|
||||
|
||||
Github readme stats comes with several built-in themes (e.g. `dark`, `radical`, `merko`, `gruvbox`, `tokyonight`, `onedark`, `cobalt`, `synthwave`, `highcontrast`, `dracula`).
|
||||
GitHub readme stats comes with several built-in themes (e.g. `dark`, `radical`, `merko`, `gruvbox`, `tokyonight`, `onedark`, `cobalt`, `synthwave`, `highcontrast`, `dracula`).
|
||||
|
||||
<img src="https://res.cloudinary.com/anuraghazra/image/upload/v1595174536/grs-themes_l4ynja.png" alt="GitHub Readme Stats Themes" width="600px"/>
|
||||
|
||||
You can look at a preview for [all available themes](./themes/README.md) or checkout the [theme config file](./themes/index.js) & **you can also contribute new themes** if you like :D
|
||||
|
||||
#### Responsive Card Theme
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats#gh-dark-mode-only)
|
||||
[](https://github.com/anuraghazra/github-readme-stats#gh-light-mode-only)
|
||||
|
||||
Since GitHub will re-upload the cards and serve them from their [CDN](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-anonymized-urls), we can not infer the browser/GitHub theme on the server side. There are, however, four methods you can use to create dynamics themes on the client side.
|
||||
|
||||
##### Use the transparent theme
|
||||
|
||||
We have included a `transparent` theme that has a transparent background. This theme is optimized to look good on GitHub's dark and light default themes. You can enable this theme using the `&theme=transparent` parameter like so:
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>:eyes: Show example</summary>
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
##### 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`):
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>:eyes: Show example</summary>
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
##### Use GitHub's theme context tag
|
||||
|
||||
You can use [GitHub's theme context](https://github.blog/changelog/2021-11-24-specify-theme-context-for-images-in-markdown/) tags to switch the theme based on the user GitHub theme automatically. This is done by appending `#gh-dark-mode-only` or `#gh-light-mode-only` to the end of an image URL. This tag will define whether the image specified in the markdown is only shown to viewers using a light or a dark GitHub theme:
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats#gh-dark-mode-only)
|
||||
[](https://github.com/anuraghazra/github-readme-stats#gh-light-mode-only)
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>:eyes: Show example</summary>
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats#gh-dark-mode-only)
|
||||
[](https://github.com/anuraghazra/github-readme-stats#gh-light-mode-only)
|
||||
|
||||
</details>
|
||||
|
||||
##### 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 `<picture>` element in combination with the `prefers-color-scheme` media feature.
|
||||
|
||||
```html
|
||||
<picture>
|
||||
<source
|
||||
srcset="https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<source
|
||||
srcset="https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true"
|
||||
media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
|
||||
/>
|
||||
<img src="https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true" />
|
||||
</picture>
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>:eyes: Show example</summary>
|
||||
|
||||
<picture>
|
||||
<source
|
||||
srcset="https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<source
|
||||
srcset="https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true"
|
||||
media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
|
||||
/>
|
||||
<img src="https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true" />
|
||||
</picture>
|
||||
|
||||
</details>
|
||||
|
||||
### Customization
|
||||
|
||||
You can customize the appearance of your `Stats Card` or `Repo Card` however you wish with URL parameters.
|
||||
@@ -169,12 +261,12 @@ You can customize the appearance of your `Stats Card` or `Repo Card` however you
|
||||
- `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.
|
||||
- `cache_seconds` - set the cache header manually _(min: 7200, max: 86400)_. Default: `14400 seconds (4 hours)`.
|
||||
- `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`.
|
||||
|
||||
> **Warning**
|
||||
> We use caching to decrease the load on our servers (see https://github.com/anuraghazra/github-readme-stats/issues/1471#issuecomment-1271551425). Our cards have a default cache of 4 hours (14400 seconds). Also, note that the cache is clamped to a minimum of 4 hours and a maximum of 24 hours.
|
||||
> We use caching to decrease the load on our servers (see <https://github.com/anuraghazra/github-readme-stats/issues/1471#issuecomment-1271551425>). Our cards have a default cache of 4 hours (14400 seconds). Also, note that the cache is clamped to a minimum of 4 hours and a maximum of 24 hours.
|
||||
|
||||
##### Gradient in bg_color
|
||||
|
||||
@@ -193,9 +285,10 @@ You can provide multiple comma-separated values in the bg_color option to render
|
||||
- `count_private` - Count private commits _(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: `<username> Github Stats`.
|
||||
- `custom_title` - Sets a custom title for the card. Default: `<username> 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.
|
||||
|
||||
> **Note**
|
||||
> When hide_rank=`true`, the minimum card width is 270 px + the title length and padding.
|
||||
@@ -213,6 +306,8 @@ You can provide multiple comma-separated values in the bg_color option to render
|
||||
- `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`.
|
||||
|
||||
> **Warning**
|
||||
> Language names should be URI-escaped, as specified in [Percent Encoding](https://en.wikipedia.org/wiki/Percent-encoding)
|
||||
@@ -262,7 +357,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 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.
|
||||
|
||||
### Usage
|
||||
|
||||
@@ -306,6 +401,14 @@ You can use the `&layout=compact` option to change the card design.
|
||||
[](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)
|
||||
@@ -314,6 +417,10 @@ You can use the `&layout=compact` option to change the card design.
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
- Hidden progress bars
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
# Wakatime Week Stats
|
||||
|
||||
Change the `?username=` value to your [Wakatime](https://wakatime.com) username.
|
||||
@@ -404,9 +511,11 @@ By default, GitHub does not lay out the cards side by side. To do that, you can
|
||||
</a>
|
||||
```
|
||||
|
||||
## Deploy on your own Vercel instance
|
||||
## Deploy on your own
|
||||
|
||||
#### [Check Out Step By Step Video Tutorial By @codeSTACKr](https://youtu.be/n6d4KHSKqGk?t=107)
|
||||
### On Vercel
|
||||
|
||||
#### :film_projector: [Check Out Step By Step Video Tutorial By @codeSTACKr](https://youtu.be/n6d4KHSKqGk?t=107)
|
||||
|
||||
> **Warning**
|
||||
> If you are on the [hobby (i.e. free)](https://vercel.com/pricing) Vercel plan, please make sure you change the `maxDuration` parameter in the [vercel.json](https://github.com/anuraghazra/github-readme-stats/blob/master/vercel.json) file from `30` to `10` (see [#1416](https://github.com/anuraghazra/github-readme-stats/issues/1416#issuecomment-950275476) for more information).
|
||||
@@ -441,6 +550,23 @@ Since the GitHub API only allows 5k requests per hour, my `https://github-readme
|
||||
|
||||
</details>
|
||||
|
||||
### 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.
|
||||
|
||||
<details>
|
||||
<summary><b>:hammer_and_wrench: Step-by-step guide for deploying on other platforms</b></summary>
|
||||
|
||||
1. Fork or clone this repo as per your needs
|
||||
2. Add `express` to the dependencies section of `package.json`
|
||||
https://github.com/anuraghazra/github-readme-stats/blob/ba7c2f8b55eac8452e479c8bd38b044d204d0424/package.json#L54-L61
|
||||
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
|
||||
https://github.com/anuraghazra/github-readme-stats/blob/ba7c2f8b55eac8452e479c8bd38b044d204d0424/package.json#L11
|
||||
5. You're done 🎉
|
||||
</details>
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -9,19 +9,28 @@ import github from "@actions/github";
|
||||
import { RequestError } from "@octokit/request-error";
|
||||
import { getGithubToken, getRepoInfo } from "./helpers.js";
|
||||
|
||||
// Script parameters
|
||||
const CLOSING_COMMENT = `
|
||||
\rThis PR has been automatically closed due to inactivity. Please feel free to reopen it if you need to continue working on it.\
|
||||
\rThis theme PR has been automatically closed due to inactivity. Please reopen it if you want to continue working on it.\
|
||||
\rThank you for your contributions.
|
||||
`;
|
||||
const REVIEWER = "github-actions[bot]";
|
||||
|
||||
/**
|
||||
* Retrieve the review user.
|
||||
* @returns {string} review user.
|
||||
*/
|
||||
const getReviewer = () => {
|
||||
return process.env.REVIEWER ? process.env.REVIEWER : REVIEWER;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch open PRs from a given repository.
|
||||
* @param user The user name of the repository owner.
|
||||
* @param repo The name of the repository.
|
||||
* @param reviewer The reviewer to filter by.
|
||||
* @returns The open PRs.
|
||||
*/
|
||||
export const fetchOpenPRs = async (octokit, user, repo) => {
|
||||
export const fetchOpenPRs = async (octokit, user, repo, reviewer) => {
|
||||
const openPRs = [];
|
||||
let hasNextPage = true;
|
||||
let endCursor;
|
||||
@@ -49,9 +58,9 @@ export const fetchOpenPRs = async (octokit, user, repo) => {
|
||||
name
|
||||
}
|
||||
}
|
||||
reviews(first: 1, states: CHANGES_REQUESTED, author: "github-actions[bot]") {
|
||||
reviews(first: 100, states: CHANGES_REQUESTED, author: "${reviewer}") {
|
||||
nodes {
|
||||
updatedAt
|
||||
submittedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,11 +108,13 @@ const isStale = (pullRequest, staleDays) => {
|
||||
pullRequest.commits.nodes[0].commit.pushedDate,
|
||||
);
|
||||
if (pullRequest.reviews.nodes[0]) {
|
||||
const lastReviewDate = new Date(pullRequest.reviews.nodes[0].updatedAt);
|
||||
const lastReviewDate = new Date(
|
||||
pullRequest.reviews.nodes.sort((a, b) => (a < b ? 1 : -1))[0].submittedAt,
|
||||
);
|
||||
const lastUpdateDate =
|
||||
lastCommitDate >= lastReviewDate ? lastCommitDate : lastReviewDate;
|
||||
const now = new Date();
|
||||
return now - lastUpdateDate > 1000 * 60 * 60 * 24 * staleDays;
|
||||
return (now - lastUpdateDate) / (1000 * 60 * 60 * 24) >= staleDays;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -116,14 +127,15 @@ const run = async () => {
|
||||
try {
|
||||
// Create octokit client.
|
||||
const dryRun = process.env.DRY_RUN === "true" || false;
|
||||
const staleDays = process.env.STALE_DAYS || 15;
|
||||
const staleDays = process.env.STALE_DAYS || 20;
|
||||
debug("Creating octokit client...");
|
||||
const octokit = github.getOctokit(getGithubToken());
|
||||
const { owner, repo } = getRepoInfo(github.context);
|
||||
const reviewer = getReviewer();
|
||||
|
||||
// Retrieve all theme pull requests.
|
||||
debug("Retrieving all theme pull requests...");
|
||||
const prs = await fetchOpenPRs(octokit, owner, repo);
|
||||
const prs = await fetchOpenPRs(octokit, owner, repo, reviewer);
|
||||
const themePRs = pullsWithLabel(prs, "themes");
|
||||
const invalidThemePRs = pullsWithLabel(themePRs, "invalid");
|
||||
debug("Retrieving stale theme PRs...");
|
||||
|
||||
+1
-2
@@ -4,7 +4,6 @@
|
||||
|
||||
import { getInput } from "@actions/core";
|
||||
|
||||
// Script variables.
|
||||
const OWNER = "anuraghazra";
|
||||
const REPO = "github-readme-stats";
|
||||
|
||||
@@ -31,7 +30,7 @@ export const getRepoInfo = (ctx) => {
|
||||
/**
|
||||
* Retrieve github token and throw error if it is not found.
|
||||
*
|
||||
* @returns {string} Github token.
|
||||
* @returns {string} GitHub token.
|
||||
*/
|
||||
export const getGithubToken = () => {
|
||||
const token = getInput("github_token") || process.env.GITHUB_TOKEN;
|
||||
|
||||
@@ -16,7 +16,6 @@ import { isValidHexColor } from "../src/common/utils.js";
|
||||
import { themes } from "../themes/index.js";
|
||||
import { getGithubToken, getRepoInfo } from "./helpers.js";
|
||||
|
||||
// Script variables.
|
||||
const COMMENTER = "github-actions[bot]";
|
||||
|
||||
const COMMENT_TITLE = "Automated Theme Preview";
|
||||
@@ -25,7 +24,7 @@ const THEME_PR_SUCCESS_TEXT =
|
||||
":heavy_check_mark: Theme PR does adhere to our guidelines.";
|
||||
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 15 days** if no changes are made. After this time, you must re-open the PR for it to be reviewed.
|
||||
\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 = `
|
||||
\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).
|
||||
@@ -93,7 +92,7 @@ const isPreviewComment = (inputs, comment) => {
|
||||
* @param {number} issueNumber Issue number.
|
||||
* @param {string} repo Repository name.
|
||||
* @param {string} owner Owner of the repository.
|
||||
* @returns {Object} The Github comment object.
|
||||
* @returns {Object} The GitHub comment object.
|
||||
*/
|
||||
const findComment = async (octokit, issueNumber, owner, repo, commenter) => {
|
||||
const parameters = {
|
||||
|
||||
@@ -5,10 +5,10 @@ set -e
|
||||
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 user.name "GitHub Readme Stats Bot"
|
||||
git branch -d $BRANCH_NAME || true
|
||||
git checkout -b $BRANCH_NAME
|
||||
git add --all
|
||||
git commit --message "docs(theme): Auto update theme readme" || exit 0
|
||||
git commit --no-verify --message "docs(theme): Auto update theme readme"
|
||||
git remote add origin-$BRANCH_NAME https://${PERSONAL_TOKEN}@github.com/${GH_REPO}.git
|
||||
git push --force --quiet --set-upstream origin-$BRANCH_NAME $BRANCH_NAME
|
||||
|
||||
+10
-6
@@ -9,7 +9,7 @@
|
||||
* @param {number} to The value to calculate the probability for.
|
||||
* @returns {number} Probability.
|
||||
*/
|
||||
function normalcdf(mean, sigma, to) {
|
||||
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;
|
||||
@@ -24,7 +24,7 @@ function normalcdf(mean, sigma, to) {
|
||||
sign = -1;
|
||||
}
|
||||
return (1 / 2) * (1 + sign * erf);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the users rank.
|
||||
@@ -38,7 +38,7 @@ function normalcdf(mean, sigma, to) {
|
||||
* @param {number} stargazers The number of stars.
|
||||
* @returns {{level: string, score: number}}} The users rank.
|
||||
*/
|
||||
function calculateRank({
|
||||
const calculateRank = ({
|
||||
totalRepos,
|
||||
totalCommits,
|
||||
contributions,
|
||||
@@ -46,7 +46,7 @@ function calculateRank({
|
||||
prs,
|
||||
issues,
|
||||
stargazers,
|
||||
}) {
|
||||
}) => {
|
||||
const COMMITS_OFFSET = 1.65;
|
||||
const CONTRIBS_OFFSET = 1.65;
|
||||
const ISSUES_OFFSET = 1;
|
||||
@@ -70,7 +70,11 @@ function calculateRank({
|
||||
const RANK_B_VALUE = 100;
|
||||
|
||||
const TOTAL_VALUES =
|
||||
RANK_S_VALUE + RANK_A2_VALUE + RANK_A3_VALUE + RANK_B_VALUE;
|
||||
RANK_S_VALUE +
|
||||
RANK_DOUBLE_A_VALUE +
|
||||
RANK_A2_VALUE +
|
||||
RANK_A3_VALUE +
|
||||
RANK_B_VALUE;
|
||||
|
||||
// prettier-ignore
|
||||
const score = (
|
||||
@@ -94,7 +98,7 @@ function calculateRank({
|
||||
})();
|
||||
|
||||
return { level, score: normalizedScore };
|
||||
}
|
||||
};
|
||||
|
||||
export { calculateRank };
|
||||
export default calculateRank;
|
||||
|
||||
+33
-16
@@ -12,6 +12,11 @@ import {
|
||||
import { getStyles } from "../getStyles.js";
|
||||
import { statCardLocales } from "../translations.js";
|
||||
|
||||
const CARD_MIN_WIDTH = 287;
|
||||
const CARD_DEFAULT_WIDTH = 287;
|
||||
const RANK_CARD_MIN_WIDTH = 420;
|
||||
const RANK_CARD_DEFAULT_WIDTH = 450;
|
||||
|
||||
/**
|
||||
* Create a stats card text item.
|
||||
*
|
||||
@@ -89,6 +94,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
include_all_commits = false,
|
||||
line_height = 25,
|
||||
title_color,
|
||||
ring_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
text_bold = true,
|
||||
@@ -104,13 +110,14 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
const lheight = parseInt(String(line_height), 10);
|
||||
|
||||
// returns theme based colors with proper overrides and defaults
|
||||
const { titleColor, textColor, iconColor, bgColor, borderColor } =
|
||||
const { titleColor, iconColor, textColor, bgColor, borderColor, ringColor } =
|
||||
getCardColors({
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
icon_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
ring_color,
|
||||
theme,
|
||||
});
|
||||
|
||||
@@ -152,7 +159,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
},
|
||||
contribs: {
|
||||
icon: icons.contribs,
|
||||
label: i18n.t("statcard.contribs"),
|
||||
label: i18n.t("statcard.contribs") + " (last year)",
|
||||
value: contributedTo,
|
||||
id: "contribs",
|
||||
},
|
||||
@@ -170,6 +177,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
"pl",
|
||||
"de",
|
||||
"nl",
|
||||
"zh-tw",
|
||||
];
|
||||
const isLongLocale = longLocales.includes(locale) === true;
|
||||
|
||||
@@ -182,8 +190,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
...STATS[key],
|
||||
index,
|
||||
showIcons: show_icons,
|
||||
shiftValuePos:
|
||||
(!include_all_commits ? 50 : 35) + (isLongLocale ? 50 : 0),
|
||||
shiftValuePos: 79.01 + (isLongLocale ? 50 : 0),
|
||||
bold: text_bold,
|
||||
}),
|
||||
);
|
||||
@@ -200,6 +207,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
const progress = 100 - rank.score;
|
||||
const cssStyles = getStyles({
|
||||
titleColor,
|
||||
ringColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
show_icons,
|
||||
@@ -215,11 +223,17 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
When hide_rank=false, the minimum card_width is 340 px + the icon width (if show_icons=true).
|
||||
Numbers are picked by looking at existing dimensions on production.
|
||||
*/
|
||||
const iconWidth = show_icons ? 16 : 0;
|
||||
const minCardWidth = hide_rank
|
||||
? clampValue(50 /* padding */ + calculateTextWidth() * 2, 270, Infinity)
|
||||
: 340 + iconWidth;
|
||||
const defaultCardWidth = hide_rank ? 270 : 495;
|
||||
const iconWidth = show_icons ? 16 + /* padding */ 1 : 0;
|
||||
const minCardWidth =
|
||||
(hide_rank
|
||||
? clampValue(
|
||||
50 /* padding */ + calculateTextWidth() * 2,
|
||||
CARD_MIN_WIDTH,
|
||||
Infinity,
|
||||
)
|
||||
: RANK_CARD_MIN_WIDTH) + iconWidth;
|
||||
const defaultCardWidth =
|
||||
(hide_rank ? CARD_DEFAULT_WIDTH : RANK_CARD_DEFAULT_WIDTH) + iconWidth;
|
||||
let width = isNaN(card_width) ? defaultCardWidth : card_width;
|
||||
if (width < minCardWidth) {
|
||||
width = minCardWidth;
|
||||
@@ -248,18 +262,21 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
|
||||
/**
|
||||
* Calculates the right rank circle translation values such that the rank circle
|
||||
* keeps respecting the padding.
|
||||
* keeps respecting the following padding:
|
||||
*
|
||||
* width > 450: The default left padding of 50 px will be used.
|
||||
* width < 450: The left and right padding will shrink equally.
|
||||
* width > RANK_CARD_DEFAULT_WIDTH: The default right padding of 70 px will be used.
|
||||
* width < RANK_CARD_DEFAULT_WIDTH: The left and right padding will be enlarged
|
||||
* equally from a certain minimum at RANK_CARD_MIN_WIDTH.
|
||||
*
|
||||
* @returns {number} - Rank circle translation value.
|
||||
*/
|
||||
const calculateRankXTranslation = () => {
|
||||
if (width < 450) {
|
||||
return width - 95 + (45 * (450 - 340)) / 110;
|
||||
const minXTranslation = RANK_CARD_MIN_WIDTH + iconWidth - 70;
|
||||
if (width > RANK_CARD_DEFAULT_WIDTH) {
|
||||
const xMaxExpansion = minXTranslation + (450 - minCardWidth) / 2;
|
||||
return xMaxExpansion + width - RANK_CARD_DEFAULT_WIDTH;
|
||||
} else {
|
||||
return width - 95;
|
||||
return minXTranslation + (width - minCardWidth) / 2;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -39,49 +39,57 @@ const getLongestLang = (arr) =>
|
||||
* Creates a node to display usage of a programming language in percentage
|
||||
* using text and a horizontal progress bar.
|
||||
*
|
||||
* @param {object[]} props Function properties.
|
||||
* @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.progress Usage of the programming language in percentage.
|
||||
* @param {number} props.index Index of the programming language.
|
||||
* @returns {string} Programming language SVG node.
|
||||
*/
|
||||
const createProgressTextNode = ({ width, color, name, progress }) => {
|
||||
const createProgressTextNode = ({ width, color, name, progress, index }) => {
|
||||
const staggerDelay = (index + 3) * 150;
|
||||
const paddingRight = 95;
|
||||
const progressTextX = width - paddingRight + 10;
|
||||
const progressWidth = width - paddingRight;
|
||||
|
||||
return `
|
||||
<text data-testid="lang-name" x="2" y="15" class="lang-name">${name}</text>
|
||||
<text x="${progressTextX}" y="34" class="lang-name">${progress}%</text>
|
||||
${createProgressNode({
|
||||
x: 0,
|
||||
y: 25,
|
||||
color,
|
||||
width: progressWidth,
|
||||
progress,
|
||||
progressBarBackgroundColor: "#ddd",
|
||||
})}
|
||||
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
|
||||
<text data-testid="lang-name" x="2" y="15" class="lang-name">${name}</text>
|
||||
<text x="${progressTextX}" y="34" class="lang-name">${progress}%</text>
|
||||
${createProgressNode({
|
||||
x: 0,
|
||||
y: 25,
|
||||
color,
|
||||
width: progressWidth,
|
||||
progress,
|
||||
progressBarBackgroundColor: "#ddd",
|
||||
delay: staggerDelay + 300,
|
||||
})}
|
||||
</g>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a text only node to display usage of a programming language in percentage.
|
||||
*
|
||||
* @param {object[]} props Function properties.
|
||||
* @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 }) => {
|
||||
const createCompactLangNode = ({ lang, totalSize, hideProgress, index }) => {
|
||||
const percentage = ((lang.size / totalSize) * 100).toFixed(2);
|
||||
const staggerDelay = (index + 3) * 150;
|
||||
const color = lang.color || "#858585";
|
||||
|
||||
return `
|
||||
<g>
|
||||
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
|
||||
<circle cx="5" cy="6" r="5" fill="${color}" />
|
||||
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
||||
${lang.name} ${percentage}%
|
||||
${lang.name} ${hideProgress ? "" : percentage + "%"}
|
||||
</text>
|
||||
</g>
|
||||
`;
|
||||
@@ -93,9 +101,10 @@ const createCompactLangNode = ({ lang, totalSize }) => {
|
||||
* @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) => {
|
||||
@@ -104,7 +113,7 @@ const createLanguageTextNode = ({ langs, totalSize }) => {
|
||||
createCompactLangNode({
|
||||
lang,
|
||||
totalSize,
|
||||
// @ts-ignore
|
||||
hideProgress,
|
||||
index,
|
||||
}),
|
||||
);
|
||||
@@ -134,12 +143,13 @@ const createLanguageTextNode = ({ langs, totalSize }) => {
|
||||
*/
|
||||
const renderNormalLayout = (langs, width, totalLanguageSize) => {
|
||||
return flexLayout({
|
||||
items: langs.map((lang) => {
|
||||
items: langs.map((lang, index) => {
|
||||
return createProgressTextNode({
|
||||
width: width,
|
||||
width,
|
||||
name: lang.name,
|
||||
color: lang.color || DEFAULT_LANG_COLOR,
|
||||
progress: ((lang.size / totalLanguageSize) * 100).toFixed(2),
|
||||
index,
|
||||
});
|
||||
}),
|
||||
gap: 40,
|
||||
@@ -153,9 +163,10 @@ const renderNormalLayout = (langs, width, totalLanguageSize) => {
|
||||
* @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
|
||||
@@ -186,15 +197,21 @@ const renderCompactLayout = (langs, width, totalLanguageSize) => {
|
||||
.join("");
|
||||
|
||||
return `
|
||||
<mask id="rect-mask">
|
||||
<rect x="0" y="0" width="${offsetWidth}" height="8" fill="white" rx="5" />
|
||||
${
|
||||
!hideProgress
|
||||
? `
|
||||
<mask id="rect-mask">
|
||||
<rect x="0" y="0" width="${offsetWidth}" height="8" fill="white" rx="5"/>
|
||||
</mask>
|
||||
${compactProgressBar}
|
||||
|
||||
<g transform="translate(0, 25)">
|
||||
`
|
||||
: ""
|
||||
}
|
||||
<g transform="translate(0, ${hideProgress ? "0" : "25"})">
|
||||
${createLanguageTextNode({
|
||||
langs,
|
||||
totalSize: totalLanguageSize,
|
||||
hideProgress: hideProgress,
|
||||
})}
|
||||
</g>
|
||||
`;
|
||||
@@ -269,6 +286,7 @@ const renderTopLanguages = (topLangs, options = {}) => {
|
||||
text_color,
|
||||
bg_color,
|
||||
hide,
|
||||
hide_progress,
|
||||
theme,
|
||||
layout,
|
||||
custom_title,
|
||||
@@ -276,6 +294,7 @@ const renderTopLanguages = (topLangs, options = {}) => {
|
||||
langs_count = DEFAULT_LANGS_COUNT,
|
||||
border_radius,
|
||||
border_color,
|
||||
disable_animations,
|
||||
} = options;
|
||||
|
||||
const i18n = new I18n({
|
||||
@@ -297,11 +316,17 @@ const renderTopLanguages = (topLangs, options = {}) => {
|
||||
let height = calculateNormalLayoutHeight(langs.length);
|
||||
|
||||
let finalLayout = "";
|
||||
if (layout === "compact") {
|
||||
if (layout === "compact" || hide_progress == true) {
|
||||
width = width + 50; // padding
|
||||
height = calculateCompactLayoutHeight(langs.length);
|
||||
height =
|
||||
calculateCompactLayoutHeight(langs.length) + (hide_progress ? -25 : 0);
|
||||
|
||||
finalLayout = renderCompactLayout(langs, width, totalLanguageSize);
|
||||
finalLayout = renderCompactLayout(
|
||||
langs,
|
||||
width,
|
||||
totalLanguageSize,
|
||||
hide_progress,
|
||||
);
|
||||
} else {
|
||||
finalLayout = renderNormalLayout(langs, width, totalLanguageSize);
|
||||
}
|
||||
@@ -324,11 +349,43 @@ const renderTopLanguages = (topLangs, options = {}) => {
|
||||
colors,
|
||||
});
|
||||
|
||||
card.disableAnimations();
|
||||
if (disable_animations) card.disableAnimations();
|
||||
|
||||
card.setHideBorder(hide_border);
|
||||
card.setHideTitle(hide_title);
|
||||
card.setCSS(
|
||||
`.lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.textColor} }`,
|
||||
`
|
||||
@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: ${colors.textColor};
|
||||
}
|
||||
.stagger {
|
||||
opacity: 0;
|
||||
animation: fadeInAnimation 0.3s ease-in-out forwards;
|
||||
}
|
||||
#rect-mask rect{
|
||||
animation: slideInAnimation 1s ease-in-out forwards;
|
||||
}
|
||||
.lang-progress{
|
||||
animation: growWidthAnimation 0.6s ease-in-out forwards;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
return card.render(`
|
||||
|
||||
Vendored
+2
@@ -37,6 +37,8 @@ export type TopLangOptions = CommonOptions & {
|
||||
layout: "compact" | "normal";
|
||||
custom_title: string;
|
||||
langs_count: number;
|
||||
disable_animations: boolean;
|
||||
hide_progress: boolean;
|
||||
};
|
||||
|
||||
type WakaTimeOptions = CommonOptions & {
|
||||
|
||||
@@ -159,7 +159,7 @@ const recalculatePercentages = (languages) => {
|
||||
* @returns {string} WakaTime card SVG.
|
||||
*/
|
||||
const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
|
||||
let { languages } = stats;
|
||||
let { languages = [] } = stats;
|
||||
const {
|
||||
hide_title = false,
|
||||
hide_border = false,
|
||||
@@ -174,20 +174,24 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
|
||||
custom_title,
|
||||
locale,
|
||||
layout,
|
||||
langs_count = languages ? languages.length : 0,
|
||||
langs_count = languages.length,
|
||||
border_radius,
|
||||
border_color,
|
||||
} = options;
|
||||
|
||||
const shouldHideLangs = Array.isArray(hide) && hide.length > 0;
|
||||
if (shouldHideLangs && languages !== undefined) {
|
||||
if (shouldHideLangs) {
|
||||
const languagesToHide = new Set(hide.map((lang) => lowercaseTrim(lang)));
|
||||
languages = languages.filter(
|
||||
(lang) => !languagesToHide.has(lowercaseTrim(lang.name)),
|
||||
);
|
||||
recalculatePercentages(languages);
|
||||
}
|
||||
|
||||
// Since the percentages are sorted in descending order, we can just
|
||||
// slice from the beginning without sorting.
|
||||
languages = languages.slice(0, langs_count);
|
||||
recalculatePercentages(languages);
|
||||
|
||||
const i18n = new I18n({
|
||||
locale,
|
||||
translations: wakatimeCardLocales,
|
||||
@@ -209,10 +213,8 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
|
||||
});
|
||||
|
||||
const filteredLanguages = languages
|
||||
? languages
|
||||
.filter((language) => language.hours || language.minutes)
|
||||
.slice(0, langsCount)
|
||||
: [];
|
||||
.filter((language) => language.hours || language.minutes)
|
||||
.slice(0, langsCount);
|
||||
|
||||
// Calculate the card height depending on how many items there are
|
||||
// but if rank circle is visible clamp the minimum height to `150`
|
||||
|
||||
@@ -10,6 +10,7 @@ import { clampValue } from "./utils.js";
|
||||
* @param {string} createProgressNodeParams.color Progress color.
|
||||
* @param {string} createProgressNodeParams.progress Progress value.
|
||||
* @param {string} createProgressNodeParams.progressBarBackgroundColor Progress bar bg color.
|
||||
* @param {number} createProgressNodeParams.delay Delay before animation starts.
|
||||
* @returns {string} Progress node.
|
||||
*/
|
||||
const createProgressNode = ({
|
||||
@@ -19,20 +20,22 @@ const createProgressNode = ({
|
||||
color,
|
||||
progress,
|
||||
progressBarBackgroundColor,
|
||||
delay,
|
||||
}) => {
|
||||
const progressPercentage = clampValue(progress, 2, 100);
|
||||
|
||||
return `
|
||||
<svg width="${width}" x="${x}" y="${y}">
|
||||
<rect rx="5" ry="5" x="0" y="0" width="${width}" height="8" fill="${progressBarBackgroundColor}"></rect>
|
||||
<rect
|
||||
height="8"
|
||||
fill="${color}"
|
||||
rx="5" ry="5" x="0" y="0"
|
||||
data-testid="lang-progress"
|
||||
width="${progressPercentage}%"
|
||||
>
|
||||
</rect>
|
||||
<svg data-testid="lang-progress" width="${progressPercentage}%">
|
||||
<rect
|
||||
height="8"
|
||||
fill="${color}"
|
||||
rx="5" ry="5" x="0" y="0"
|
||||
class="lang-progress"
|
||||
style="animation-delay: ${delay}ms;"
|
||||
/>
|
||||
</svg>
|
||||
</svg>
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
"Csound Score": "#1a1a1a",
|
||||
"Cuda": "#3A4E3A",
|
||||
"Curry": "#531242",
|
||||
"Cypher": "#34c0eb",
|
||||
"Cython": "#fedf5b",
|
||||
"D": "#ba595e",
|
||||
"DM": "#447265",
|
||||
@@ -124,6 +125,7 @@
|
||||
"Earthly": "#2af0ff",
|
||||
"Easybuild": "#069406",
|
||||
"Ecere Projects": "#913960",
|
||||
"Ecmarkup": "#eb8131",
|
||||
"EditorConfig": "#fff1f2",
|
||||
"Eiffel": "#4d6977",
|
||||
"Elixir": "#6e4a7e",
|
||||
@@ -215,6 +217,7 @@
|
||||
"Idris": "#b30000",
|
||||
"Ignore List": "#000000",
|
||||
"ImageJ Macro": "#99AAFF",
|
||||
"Imba": "#16cec6",
|
||||
"Inno Setup": "#264b99",
|
||||
"Io": "#a9188d",
|
||||
"Ioke": "#078193",
|
||||
@@ -286,6 +289,7 @@
|
||||
"Mathematica": "#dd1100",
|
||||
"Max": "#c4a79c",
|
||||
"Mercury": "#ff2b2b",
|
||||
"Mermaid": "#ff3670",
|
||||
"Meson": "#007800",
|
||||
"Metal": "#8f14e9",
|
||||
"MiniYAML": "#ff1111",
|
||||
@@ -318,6 +322,10 @@
|
||||
"Nu": "#c9df40",
|
||||
"NumPy": "#9C8AF9",
|
||||
"Nunjucks": "#3d8137",
|
||||
"OASv2-json": "#85ea2d",
|
||||
"OASv2-yaml": "#85ea2d",
|
||||
"OASv3-json": "#85ea2d",
|
||||
"OASv3-yaml": "#85ea2d",
|
||||
"OCaml": "#3be133",
|
||||
"ObjectScript": "#424893",
|
||||
"Objective-C": "#438eff",
|
||||
@@ -327,14 +335,18 @@
|
||||
"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",
|
||||
@@ -351,6 +363,7 @@
|
||||
"PigLatin": "#fcd7de",
|
||||
"Pike": "#005390",
|
||||
"PogoScript": "#d80074",
|
||||
"Polar": "#ae81ff",
|
||||
"Portugol": "#f8bd00",
|
||||
"PostCSS": "#dc3a0c",
|
||||
"PostScript": "#da291c",
|
||||
@@ -414,6 +427,7 @@
|
||||
"Sass": "#a53b70",
|
||||
"Scala": "#c22d40",
|
||||
"Scaml": "#bd181a",
|
||||
"Scenic": "#fdc700",
|
||||
"Scheme": "#1e4aec",
|
||||
"Scilab": "#ca0f21",
|
||||
"Self": "#0579aa",
|
||||
@@ -421,6 +435,7 @@
|
||||
"Shell": "#89e051",
|
||||
"ShellCheck Config": "#cecfcb",
|
||||
"Shen": "#120F14",
|
||||
"Simple File Verification": "#C9BFED",
|
||||
"Singularity": "#64E6AD",
|
||||
"Slash": "#007eff",
|
||||
"Slice": "#003fa2",
|
||||
@@ -428,6 +443,7 @@
|
||||
"SmPL": "#c94949",
|
||||
"Smalltalk": "#596706",
|
||||
"Smarty": "#f0c040",
|
||||
"Smithy": "#c44536",
|
||||
"Solidity": "#AA6746",
|
||||
"SourcePawn": "#f69e1d",
|
||||
"Squirrel": "#800000",
|
||||
@@ -478,6 +494,7 @@
|
||||
"Vim Script": "#199f4b",
|
||||
"Vim Snippet": "#199f4b",
|
||||
"Visual Basic .NET": "#945db7",
|
||||
"Visual Basic 6.0": "#2c6353",
|
||||
"Volt": "#1F1F1F",
|
||||
"Vue": "#41b883",
|
||||
"Vyper": "#2980b9",
|
||||
@@ -514,6 +531,7 @@
|
||||
"fish": "#4aae47",
|
||||
"hoon": "#00b171",
|
||||
"jq": "#c7254e",
|
||||
"just": "#384d54",
|
||||
"kvlang": "#1da6e0",
|
||||
"mIRC Script": "#3d57c3",
|
||||
"mcfunction": "#E22837",
|
||||
|
||||
+11
-2
@@ -1,5 +1,11 @@
|
||||
import { CustomError, logger } from "./utils.js";
|
||||
|
||||
// Script variables.
|
||||
const PATs = Object.keys(process.env).filter((key) =>
|
||||
/PAT_\d*$/.exec(key),
|
||||
).length;
|
||||
const RETRIES = PATs ? PATs : 7;
|
||||
|
||||
/**
|
||||
* Try to execute the fetcher function until it succeeds or the max number of retries is reached.
|
||||
*
|
||||
@@ -10,7 +16,7 @@ import { CustomError, logger } from "./utils.js";
|
||||
* @returns Promise<retryer>
|
||||
*/
|
||||
const retryer = async (fetcher, variables, retries = 0) => {
|
||||
if (retries > 7) {
|
||||
if (retries > RETRIES) {
|
||||
throw new CustomError("Maximum retries exceeded", CustomError.MAX_RETRY);
|
||||
}
|
||||
try {
|
||||
@@ -39,8 +45,11 @@ const retryer = async (fetcher, variables, retries = 0) => {
|
||||
// prettier-ignore
|
||||
// also checking for bad credentials if any tokens gets invalidated
|
||||
const isBadCredential = err.response.data && err.response.data.message === "Bad credentials";
|
||||
const isAccountSuspended =
|
||||
err.response.data &&
|
||||
err.response.data.message === "Sorry. Your account was suspended.";
|
||||
|
||||
if (isBadCredential) {
|
||||
if (isBadCredential || isAccountSuspended) {
|
||||
logger.log(`PAT_${retries + 1} Failed`);
|
||||
retries++;
|
||||
// directly return from the function
|
||||
|
||||
+66
-41
@@ -42,13 +42,13 @@ const renderError = (message, secondaryMessage = "") => {
|
||||
* @param {string} str String to encode.
|
||||
* @returns {string} Encoded string.
|
||||
*/
|
||||
function encodeHTML(str) {
|
||||
const encodeHTML = (str) => {
|
||||
return str
|
||||
.replace(/[\u00A0-\u9999<>&](?!#)/gim, (i) => {
|
||||
return "&#" + i.charCodeAt(0) + ";";
|
||||
})
|
||||
.replace(/\u0008/gim, "");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves num with suffix k(thousands) precise to 1 decimal if greater than 999.
|
||||
@@ -56,11 +56,11 @@ function encodeHTML(str) {
|
||||
* @param {number} num The number to format.
|
||||
* @returns {string|number} The formatted number.
|
||||
*/
|
||||
function kFormatter(num) {
|
||||
const kFormatter = (num) => {
|
||||
return Math.abs(num) > 999
|
||||
? Math.sign(num) * parseFloat((Math.abs(num) / 1000).toFixed(1)) + "k"
|
||||
: Math.sign(num) * Math.abs(num);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a string is a valid hex color.
|
||||
@@ -68,27 +68,30 @@ function kFormatter(num) {
|
||||
* @param {string} hexColor String to check.
|
||||
* @returns {boolean} True if the given string is a valid hex color.
|
||||
*/
|
||||
function isValidHexColor(hexColor) {
|
||||
const isValidHexColor = (hexColor) => {
|
||||
return new RegExp(
|
||||
/^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4})$/,
|
||||
).test(hexColor);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns boolean if value is either "true" or "false" else the value as it is.
|
||||
*
|
||||
* @param {string} value The value to parse.
|
||||
* @returns {boolean | string} The parsed value.
|
||||
* @param {string | boolean} value The value to parse.
|
||||
* @returns {boolean | undefined } The parsed value.
|
||||
*/
|
||||
function parseBoolean(value) {
|
||||
if (value === "true") {
|
||||
return true;
|
||||
} else if (value === "false") {
|
||||
return false;
|
||||
} else {
|
||||
return value;
|
||||
const parseBoolean = (value) => {
|
||||
if (typeof value === "boolean") return value;
|
||||
|
||||
if (typeof value === "string") {
|
||||
if (value.toLowerCase() === "true") {
|
||||
return true;
|
||||
} else if (value.toLowerCase() === "false") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse string to array of strings.
|
||||
@@ -96,10 +99,10 @@ function parseBoolean(value) {
|
||||
* @param {string} str The string to parse.
|
||||
* @returns {string[]} The array of strings.
|
||||
*/
|
||||
function parseArray(str) {
|
||||
const parseArray = (str) => {
|
||||
if (!str) return [];
|
||||
return str.split(",");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clamp the given number between the given range.
|
||||
@@ -109,11 +112,11 @@ function parseArray(str) {
|
||||
* @param {number} max The maximum value.
|
||||
* returns {number} The clamped number.
|
||||
*/
|
||||
function clampValue(number, min, max) {
|
||||
const clampValue = (number, min, max) => {
|
||||
// @ts-ignore
|
||||
if (Number.isNaN(parseInt(number))) return min;
|
||||
return Math.max(min, Math.min(number, max));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the given string is a valid gradient.
|
||||
@@ -121,9 +124,9 @@ function clampValue(number, min, max) {
|
||||
* @param {string[]} colors Array of colors.
|
||||
* @returns {boolean} True if the given string is a valid gradient.
|
||||
*/
|
||||
function isValidGradient(colors) {
|
||||
const isValidGradient = (colors) => {
|
||||
return isValidHexColor(colors[1]) && isValidHexColor(colors[2]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves a gradient if color has more than one valid hex codes else a single color.
|
||||
@@ -132,10 +135,10 @@ function isValidGradient(colors) {
|
||||
* @param {string} fallbackColor The fallback color.
|
||||
* @returns {string | string[]} The gradient or color.
|
||||
*/
|
||||
function fallbackColor(color, fallbackColor) {
|
||||
let colors = color.split(",");
|
||||
const fallbackColor = (color, fallbackColor) => {
|
||||
let gradient = null;
|
||||
|
||||
let colors = color ? color.split(",") : [];
|
||||
if (colors.length > 1 && isValidGradient(colors)) {
|
||||
gradient = colors;
|
||||
}
|
||||
@@ -144,7 +147,7 @@ function fallbackColor(color, fallbackColor) {
|
||||
(gradient ? gradient : isValidHexColor(color) && `#${color}`) ||
|
||||
fallbackColor
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Send GraphQL request to GitHub API.
|
||||
@@ -153,7 +156,7 @@ function fallbackColor(color, fallbackColor) {
|
||||
* @param {import('axios').AxiosRequestConfig['headers']} headers Request headers.
|
||||
* @returns {Promise<any>} Request response.
|
||||
*/
|
||||
function request(data, headers) {
|
||||
const request = (data, headers) => {
|
||||
// @ts-ignore
|
||||
return axios({
|
||||
url: "https://api.github.com/graphql",
|
||||
@@ -161,7 +164,7 @@ function request(data, headers) {
|
||||
headers,
|
||||
data,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Auto layout utility, allows us to layout things vertically or horizontally with
|
||||
@@ -174,7 +177,7 @@ function request(data, headers) {
|
||||
* @param {"column" | "row"?=} props.direction Direction to layout items.
|
||||
* @returns {string[]} Array of items with proper layout.
|
||||
*/
|
||||
function flexLayout({ items, gap, direction, sizes = [] }) {
|
||||
const flexLayout = ({ items, gap, direction, sizes = [] }) => {
|
||||
let lastSize = 0;
|
||||
// filter() for filtering out empty strings
|
||||
return items.filter(Boolean).map((item, i) => {
|
||||
@@ -186,7 +189,7 @@ function flexLayout({ items, gap, direction, sizes = [] }) {
|
||||
lastSize += size + gap;
|
||||
return `<g transform="${transform}">${item}</g>`;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns theme based colors with proper overrides and defaults.
|
||||
@@ -201,15 +204,16 @@ function flexLayout({ items, gap, direction, sizes = [] }) {
|
||||
* @param {string} args.fallbackTheme Fallback theme.
|
||||
*
|
||||
*/
|
||||
function getCardColors({
|
||||
const getCardColors = ({
|
||||
title_color,
|
||||
text_color,
|
||||
icon_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
ring_color,
|
||||
theme,
|
||||
fallbackTheme = "default",
|
||||
}) {
|
||||
}) => {
|
||||
const defaultTheme = themes[fallbackTheme];
|
||||
const selectedTheme = themes[theme] || defaultTheme;
|
||||
const defaultBorderColor =
|
||||
@@ -221,6 +225,13 @@ function getCardColors({
|
||||
title_color || selectedTheme.title_color,
|
||||
"#" + defaultTheme.title_color,
|
||||
);
|
||||
|
||||
// get the color provided by the user else the theme color
|
||||
// finally if both colors are invalid we use the titleColor
|
||||
const ringColor = fallbackColor(
|
||||
ring_color || selectedTheme.ring_color,
|
||||
titleColor,
|
||||
);
|
||||
const iconColor = fallbackColor(
|
||||
icon_color || selectedTheme.icon_color,
|
||||
"#" + defaultTheme.icon_color,
|
||||
@@ -239,8 +250,8 @@ function getCardColors({
|
||||
"#" + defaultBorderColor,
|
||||
);
|
||||
|
||||
return { titleColor, iconColor, textColor, bgColor, borderColor };
|
||||
}
|
||||
return { titleColor, iconColor, textColor, bgColor, borderColor, ringColor };
|
||||
};
|
||||
|
||||
/**
|
||||
* Split text over multiple lines based on the card width.
|
||||
@@ -250,7 +261,7 @@ function getCardColors({
|
||||
* @param {number} maxLines Maximum number of lines.
|
||||
* @returns {string[]} Array of lines.
|
||||
*/
|
||||
function wrapTextMultiline(text, width = 59, maxLines = 3) {
|
||||
const wrapTextMultiline = (text, width = 59, maxLines = 3) => {
|
||||
const fullWidthComma = ",";
|
||||
const encoded = encodeHTML(text);
|
||||
const isChinese = encoded.includes(fullWidthComma);
|
||||
@@ -275,7 +286,7 @@ function wrapTextMultiline(text, width = 59, maxLines = 3) {
|
||||
// Remove empty lines if text fits in less than maxLines lines
|
||||
const multiLineText = lines.filter(Boolean);
|
||||
return multiLineText;
|
||||
}
|
||||
};
|
||||
|
||||
const noop = () => {};
|
||||
// return console instance based on the environment
|
||||
@@ -341,7 +352,7 @@ class MissingParamError extends Error {
|
||||
* @param {number} fontSize Font size.
|
||||
* @returns {number} Text length.
|
||||
*/
|
||||
function measureText(str, fontSize = 10) {
|
||||
const measureText = (str, fontSize = 10) => {
|
||||
// prettier-ignore
|
||||
const widths = [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
@@ -373,7 +384,7 @@ function measureText(str, fontSize = 10) {
|
||||
)
|
||||
.reduce((cur, acc) => acc + cur) * fontSize
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** @param {string} name */
|
||||
const lowercaseTrim = (name) => name.toLowerCase().trim();
|
||||
@@ -386,7 +397,7 @@ const lowercaseTrim = (name) => name.toLowerCase().trim();
|
||||
* @param {number} perChunk Number of languages per column.
|
||||
* @returns {Array<T>} Array of languages split in two columns.
|
||||
*/
|
||||
function chunkArray(arr, perChunk) {
|
||||
const chunkArray = (arr, perChunk) => {
|
||||
return arr.reduce((resultArray, item, index) => {
|
||||
const chunkIndex = Math.floor(index / perChunk);
|
||||
|
||||
@@ -398,7 +409,7 @@ function chunkArray(arr, perChunk) {
|
||||
|
||||
return resultArray;
|
||||
}, []);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse emoji from string.
|
||||
@@ -406,12 +417,25 @@ function chunkArray(arr, perChunk) {
|
||||
* @param {string} str String to parse emoji from.
|
||||
* @returns {string} String with emoji parsed.
|
||||
*/
|
||||
function parseEmojis(str) {
|
||||
const parseEmojis = (str) => {
|
||||
if (!str) throw new Error("[parseEmoji]: str argument not provided");
|
||||
return str.replace(/:\w+:/gm, (emoji) => {
|
||||
return toEmoji.get(emoji) || "";
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@@ -436,4 +460,5 @@ export {
|
||||
lowercaseTrim,
|
||||
chunkArray,
|
||||
parseEmojis,
|
||||
dateDiff,
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { MissingParamError, request } from "../common/utils.js";
|
||||
* Repo data fetcher.
|
||||
*
|
||||
* @param {import('Axios').AxiosRequestHeaders} variables Fetcher variables.
|
||||
* @param {string} token Github token.
|
||||
* @param {string} token GitHub token.
|
||||
* @returns {Promise<import('Axios').AxiosResponse>} The response.
|
||||
*/
|
||||
const fetcher = (variables, token) => {
|
||||
@@ -56,11 +56,11 @@ const urlExample = "/api/pin?username=USERNAME&repo=REPO_NAME";
|
||||
/**
|
||||
* Fetch repository data.
|
||||
*
|
||||
* @param {string} username Github username.
|
||||
* @param {string} reponame Github repository name.
|
||||
* @param {string} username GitHub username.
|
||||
* @param {string} reponame GitHub repository name.
|
||||
* @returns {Promise<import("./types").RepositoryData>} Repository data.
|
||||
*/
|
||||
async function fetchRepo(username, reponame) {
|
||||
const fetchRepo = async (username, reponame) => {
|
||||
if (!username && !reponame) {
|
||||
throw new MissingParamError(["username", "repo"], urlExample);
|
||||
}
|
||||
@@ -100,7 +100,7 @@ async function fetchRepo(username, reponame) {
|
||||
starCount: data.organization.repository.stargazers.totalCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export { fetchRepo };
|
||||
export default fetchRepo;
|
||||
|
||||
+107
-111
@@ -14,46 +14,72 @@ import {
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// GraphQL queries.
|
||||
const GRAPHQL_REPOS_FIELD = `
|
||||
repositories(first: 100, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}, after: $after) {
|
||||
totalCount
|
||||
nodes {
|
||||
name
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const GRAPHQL_REPOS_QUERY = `
|
||||
query userInfo($login: String!, $after: String) {
|
||||
user(login: $login) {
|
||||
${GRAPHQL_REPOS_FIELD}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const GRAPHQL_STATS_QUERY = `
|
||||
query userInfo($login: String!, $after: String) {
|
||||
user(login: $login) {
|
||||
name
|
||||
login
|
||||
contributionsCollection {
|
||||
totalCommitContributions
|
||||
restrictedContributionsCount
|
||||
}
|
||||
repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
|
||||
totalCount
|
||||
}
|
||||
pullRequests(first: 1) {
|
||||
totalCount
|
||||
}
|
||||
openIssues: issues(states: OPEN) {
|
||||
totalCount
|
||||
}
|
||||
closedIssues: issues(states: CLOSED) {
|
||||
totalCount
|
||||
}
|
||||
followers {
|
||||
totalCount
|
||||
}
|
||||
${GRAPHQL_REPOS_FIELD}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Stats fetcher object.
|
||||
*
|
||||
* @param {import('axios').AxiosRequestHeaders} variables Fetcher variables.
|
||||
* @param {string} token Github token.
|
||||
* @returns {Promise<import('../common/types').StatsFetcherResponse>} Stats fetcher response.
|
||||
* @param {string} token GitHub token.
|
||||
* @returns {Promise<import('../common/types').Fetcher>} Stats fetcher response.
|
||||
*/
|
||||
const fetcher = (variables, token) => {
|
||||
const query = !variables.after ? GRAPHQL_STATS_QUERY : GRAPHQL_REPOS_QUERY;
|
||||
return request(
|
||||
{
|
||||
query: `
|
||||
query userInfo($login: String!) {
|
||||
user(login: $login) {
|
||||
name
|
||||
login
|
||||
contributionsCollection {
|
||||
totalCommitContributions
|
||||
restrictedContributionsCount
|
||||
}
|
||||
repositoriesContributedTo(contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
|
||||
totalCount
|
||||
}
|
||||
pullRequests {
|
||||
totalCount
|
||||
}
|
||||
openIssues: issues(states: OPEN) {
|
||||
totalCount
|
||||
}
|
||||
closedIssues: issues(states: CLOSED) {
|
||||
totalCount
|
||||
}
|
||||
followers {
|
||||
totalCount
|
||||
}
|
||||
repositories(ownerAffiliations: OWNER) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
query,
|
||||
variables,
|
||||
},
|
||||
{
|
||||
@@ -63,48 +89,51 @@ const fetcher = (variables, token) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch first 100 repositories for a given username.
|
||||
* Fetch stats information for a given username.
|
||||
*
|
||||
* @param {import('axios').AxiosRequestHeaders} variables Fetcher variables.
|
||||
* @param {string} token Github token.
|
||||
* @returns {Promise<import('../common/types').StatsFetcherResponse>} Repositories fetcher response.
|
||||
* @param {string} username Github username.
|
||||
* @returns {Promise<import('../common/types').StatsFetcher>} GraphQL Stats object.
|
||||
*
|
||||
* @description This function supports multi-page fetching if the 'FETCH_MULTI_PAGE_STARS' environment variable is set to true.
|
||||
*/
|
||||
const repositoriesFetcher = (variables, token) => {
|
||||
return request(
|
||||
{
|
||||
query: `
|
||||
query userInfo($login: String!, $after: String) {
|
||||
user(login: $login) {
|
||||
repositories(first: 100, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}, after: $after) {
|
||||
nodes {
|
||||
name
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables,
|
||||
},
|
||||
{
|
||||
Authorization: `bearer ${token}`,
|
||||
},
|
||||
);
|
||||
const statsFetcher = async (username) => {
|
||||
let stats;
|
||||
let hasNextPage = true;
|
||||
let endCursor = null;
|
||||
while (hasNextPage) {
|
||||
const variables = { login: username, first: 100, after: endCursor };
|
||||
let res = await retryer(fetcher, variables);
|
||||
if (res.data.errors) return res;
|
||||
|
||||
// Store stats data.
|
||||
const repoNodes = res.data.data.user.repositories.nodes;
|
||||
if (!stats) {
|
||||
stats = res;
|
||||
} else {
|
||||
stats.data.data.user.repositories.nodes.push(...repoNodes);
|
||||
}
|
||||
|
||||
// Disable multi page fetching on public Vercel instance due to rate limits.
|
||||
const repoNodesWithStars = repoNodes.filter(
|
||||
(node) => node.stargazers.totalCount !== 0,
|
||||
);
|
||||
hasNextPage =
|
||||
process.env.FETCH_MULTI_PAGE_STARS === "true" &&
|
||||
repoNodes.length === repoNodesWithStars.length &&
|
||||
res.data.data.user.repositories.pageInfo.hasNextPage;
|
||||
endCursor = res.data.data.user.repositories.pageInfo.endCursor;
|
||||
}
|
||||
|
||||
return stats;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch all the commits for all the repositories of a given username.
|
||||
*
|
||||
* @param {*} username Github username.
|
||||
* @param {*} username GitHub username.
|
||||
* @returns {Promise<number>} Total commits.
|
||||
*
|
||||
* @description Done like this because the Github API does not provide a way to fetch all the commits. See
|
||||
* @description Done like this because the GitHub API does not provide a way to fetch all the commits. See
|
||||
* #92#issuecomment-661026467 and #211 for more information.
|
||||
*/
|
||||
const totalCommitsFetcher = async (username) => {
|
||||
@@ -140,60 +169,20 @@ const totalCommitsFetcher = async (username) => {
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch all the stars for all the repositories of a given username.
|
||||
*
|
||||
* @param {string} username Github username.
|
||||
* @param {array} repoToHide Repositories to hide.
|
||||
* @returns {Promise<number>} Total stars.
|
||||
*/
|
||||
const totalStarsFetcher = async (username, repoToHide) => {
|
||||
let nodes = [];
|
||||
let hasNextPage = true;
|
||||
let endCursor = null;
|
||||
while (hasNextPage) {
|
||||
const variables = { login: username, first: 100, after: endCursor };
|
||||
let res = await retryer(repositoriesFetcher, variables);
|
||||
|
||||
if (res.data.errors) {
|
||||
logger.error(res.data.errors);
|
||||
throw new CustomError(
|
||||
res.data.errors[0].message || "Could not fetch user",
|
||||
CustomError.USER_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const allNodes = res.data.data.user.repositories.nodes;
|
||||
const nodesWithStars = allNodes.filter(
|
||||
(node) => node.stargazers.totalCount !== 0,
|
||||
);
|
||||
nodes.push(...nodesWithStars);
|
||||
// hasNextPage =
|
||||
// allNodes.length === nodesWithStars.length &&
|
||||
// res.data.data.user.repositories.pageInfo.hasNextPage;
|
||||
hasNextPage = false; // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
endCursor = res.data.data.user.repositories.pageInfo.endCursor;
|
||||
}
|
||||
|
||||
return nodes
|
||||
.filter((data) => !repoToHide[data.name])
|
||||
.reduce((prev, curr) => prev + curr.stargazers.totalCount, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch stats for a given username.
|
||||
*
|
||||
* @param {string} username Github username.
|
||||
* @param {string} username GitHub username.
|
||||
* @param {boolean} count_private Include private contributions.
|
||||
* @param {boolean} include_all_commits Include all commits.
|
||||
* @returns {Promise<import("./types").StatsData>} Stats data.
|
||||
*/
|
||||
async function fetchStats(
|
||||
const fetchStats = async (
|
||||
username,
|
||||
count_private = false,
|
||||
include_all_commits = false,
|
||||
exclude_repo = [],
|
||||
) {
|
||||
) => {
|
||||
if (!username) throw new MissingParamError(["username"]);
|
||||
|
||||
const stats = {
|
||||
@@ -206,7 +195,7 @@ async function fetchStats(
|
||||
rank: { level: "C", score: 0 },
|
||||
};
|
||||
|
||||
let res = await retryer(fetcher, { login: username });
|
||||
let res = await statsFetcher(username);
|
||||
|
||||
// Catch GraphQL errors.
|
||||
if (res.data.errors) {
|
||||
@@ -262,8 +251,15 @@ async function fetchStats(
|
||||
stats.contributedTo = user.repositoriesContributedTo.totalCount;
|
||||
|
||||
// Retrieve stars while filtering out repositories to be hidden
|
||||
stats.totalStars = await totalStarsFetcher(username, repoToHide);
|
||||
stats.totalStars = user.repositories.nodes
|
||||
.filter((data) => {
|
||||
return !repoToHide[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,
|
||||
@@ -275,7 +271,7 @@ async function fetchStats(
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
};
|
||||
|
||||
export { fetchStats };
|
||||
export default fetchStats;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @ts-check
|
||||
import * as dotenv from "dotenv";
|
||||
import { retryer } from "../common/retryer.js";
|
||||
import {
|
||||
CustomError,
|
||||
@@ -9,13 +8,11 @@ import {
|
||||
wrapTextMultiline,
|
||||
} from "../common/utils.js";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
/**
|
||||
* Top languages fetcher object.
|
||||
*
|
||||
* @param {import('Axios').AxiosRequestHeaders} variables Fetcher variables.
|
||||
* @param {string} token Github token.
|
||||
* @param {string} token GitHub token.
|
||||
* @returns {Promise<import('../common/types').StatsFetcherResponse>} Languages fetcher response.
|
||||
*/
|
||||
const fetcher = (variables, token) => {
|
||||
@@ -53,11 +50,11 @@ const fetcher = (variables, token) => {
|
||||
/**
|
||||
* Fetch top languages for a given username.
|
||||
*
|
||||
* @param {string} username Github username.
|
||||
* @param {string} username GitHub username.
|
||||
* @param {string[]} exclude_repo List of repositories to exclude.
|
||||
* @returns {Promise<import("./types").TopLangData>} Top languages data.
|
||||
*/
|
||||
async function fetchTopLanguages(username, exclude_repo = []) {
|
||||
const fetchTopLanguages = async (username, exclude_repo = []) => {
|
||||
if (!username) throw new MissingParamError(["username"]);
|
||||
|
||||
const res = await retryer(fetcher, { login: username });
|
||||
@@ -136,7 +133,7 @@ async function fetchTopLanguages(username, exclude_repo = []) {
|
||||
}, {});
|
||||
|
||||
return topLangs;
|
||||
}
|
||||
};
|
||||
|
||||
export { fetchTopLanguages };
|
||||
export default fetchTopLanguages;
|
||||
|
||||
+3
-2
@@ -77,6 +77,7 @@ const getStyles = ({
|
||||
titleColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
ringColor,
|
||||
show_icons,
|
||||
progress,
|
||||
}) => {
|
||||
@@ -105,13 +106,13 @@ const getStyles = ({
|
||||
}
|
||||
|
||||
.rank-circle-rim {
|
||||
stroke: ${titleColor};
|
||||
stroke: ${ringColor};
|
||||
fill: none;
|
||||
stroke-width: 6;
|
||||
opacity: 0.2;
|
||||
}
|
||||
.rank-circle {
|
||||
stroke: ${titleColor};
|
||||
stroke: ${ringColor};
|
||||
stroke-dasharray: 250;
|
||||
fill: none;
|
||||
stroke-width: 6;
|
||||
|
||||
+6
-6
@@ -37,7 +37,7 @@ const statCardLocales = ({ name, apostrophe }) => {
|
||||
sk: `GitHub štatistiky používateľa ${encodedName}`,
|
||||
tr: `${encodedName} Hesabının GitHub Yıldızları`,
|
||||
pl: `Statystyki GitHub użytkownika ${encodedName}`,
|
||||
uz: `${encodedName}ning Github'dagi statistikasi`,
|
||||
uz: `${encodedName}ning GitHub'dagi statistikasi`,
|
||||
vi: `Thống Kê GitHub ${encodedName}`,
|
||||
se: `GitHubstatistik för ${encodedName}`,
|
||||
},
|
||||
@@ -67,7 +67,7 @@ const statCardLocales = ({ name, apostrophe }) => {
|
||||
my: "Jumlah Bintang",
|
||||
sk: "Hviezdy",
|
||||
tr: "Toplam Yıldız",
|
||||
pl: "Liczba Gwiazdek dostanych",
|
||||
pl: "Liczba otrzymanych gwiazdek",
|
||||
uz: "Yulduzchalar",
|
||||
vi: "Tổng Số Sao",
|
||||
se: "Antal intjänade stjärnor",
|
||||
@@ -160,7 +160,7 @@ const statCardLocales = ({ name, apostrophe }) => {
|
||||
my: "Jumlah Isu Dilaporkan",
|
||||
sk: "Všetky problémy",
|
||||
tr: "Toplam Hata",
|
||||
pl: "Wszystkie Issues",
|
||||
pl: "Wszystkie problemy",
|
||||
uz: "'Issue'lar",
|
||||
vi: "Tổng Số Vấn Đề",
|
||||
se: "Total antal issues",
|
||||
@@ -322,7 +322,7 @@ const wakatimeCardLocales = {
|
||||
my: "Statistik Wakatime",
|
||||
sk: "Wakatime štatistika",
|
||||
tr: "Waketime İstatistikler",
|
||||
pl: "statystyki Wakatime",
|
||||
pl: "Statystyki Wakatime",
|
||||
vi: "Thống Kê Wakatime",
|
||||
se: "Wakatime statistik",
|
||||
},
|
||||
@@ -367,9 +367,9 @@ const availableLocales = Object.keys(repoCardLocales["repocard.archived"]);
|
||||
* @param {string} locale The locale to check.
|
||||
* @returns {boolean} Boolean specifying whether the locale is available or not.
|
||||
*/
|
||||
function isLocaleAvailable(locale) {
|
||||
const isLocaleAvailable = (locale) => {
|
||||
return availableLocales.includes(locale.toLowerCase());
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
isLocaleAvailable,
|
||||
|
||||
@@ -51,13 +51,13 @@ exports[`Test Render Wakatime Card should render correctly with compact layout 1
|
||||
}
|
||||
|
||||
.rank-circle-rim {
|
||||
stroke: #2f80ed;
|
||||
stroke: undefined;
|
||||
fill: none;
|
||||
stroke-width: 6;
|
||||
opacity: 0.2;
|
||||
}
|
||||
.rank-circle {
|
||||
stroke: #2f80ed;
|
||||
stroke: undefined;
|
||||
stroke-dasharray: 250;
|
||||
fill: none;
|
||||
stroke-width: 6;
|
||||
@@ -122,7 +122,7 @@ exports[`Test Render Wakatime Card should render correctly with compact layout 1
|
||||
data-testid="lang-progress"
|
||||
x="0"
|
||||
y="0"
|
||||
width="6.6495"
|
||||
width="415.61699999999996"
|
||||
height="8"
|
||||
fill="#858585"
|
||||
/>
|
||||
@@ -130,9 +130,167 @@ exports[`Test Render Wakatime Card should render correctly with compact layout 1
|
||||
<rect
|
||||
mask="url(#rect-mask)"
|
||||
data-testid="lang-progress"
|
||||
x="6.6495"
|
||||
x="415.61699999999996"
|
||||
y="0"
|
||||
width="0.465"
|
||||
width="29.0625"
|
||||
height="8"
|
||||
fill="#3178c6"
|
||||
/>
|
||||
|
||||
|
||||
<g transform="translate(25, 25)">
|
||||
<circle cx="5" cy="6" r="5" fill="#858585" />
|
||||
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
||||
Other - 19 mins
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(230, 25)">
|
||||
<circle cx="5" cy="6" r="5" fill="#3178c6" />
|
||||
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
||||
TypeScript - 1 min
|
||||
</text>
|
||||
</g>
|
||||
|
||||
|
||||
</svg>
|
||||
|
||||
</g>
|
||||
</svg>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Test Render Wakatime Card should render correctly with compact layout when langs_count is set 1`] = `
|
||||
"
|
||||
<svg
|
||||
width="495"
|
||||
height="115"
|
||||
viewBox="0 0 495 115"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
aria-labelledby="descId"
|
||||
>
|
||||
<title id="titleId"></title>
|
||||
<desc id="descId"></desc>
|
||||
<style>
|
||||
.header {
|
||||
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
|
||||
fill: #2f80ed;
|
||||
animation: fadeInAnimation 0.8s ease-in-out forwards;
|
||||
}
|
||||
@supports(-moz-appearance: auto) {
|
||||
/* Selector detects Firefox */
|
||||
.header { font-size: 15.5px; }
|
||||
}
|
||||
|
||||
|
||||
.stat {
|
||||
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: #434d58;
|
||||
}
|
||||
@supports(-moz-appearance: auto) {
|
||||
/* Selector detects Firefox */
|
||||
.stat { font-size:12px; }
|
||||
}
|
||||
.stagger {
|
||||
opacity: 0;
|
||||
animation: fadeInAnimation 0.3s ease-in-out forwards;
|
||||
}
|
||||
.rank-text {
|
||||
font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58;
|
||||
animation: scaleInAnimation 0.3s ease-in-out forwards;
|
||||
}
|
||||
|
||||
.not_bold { font-weight: 400 }
|
||||
.bold { font-weight: 700 }
|
||||
.icon {
|
||||
fill: #4c71f2;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rank-circle-rim {
|
||||
stroke: undefined;
|
||||
fill: none;
|
||||
stroke-width: 6;
|
||||
opacity: 0.2;
|
||||
}
|
||||
.rank-circle {
|
||||
stroke: undefined;
|
||||
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: #434d58 }
|
||||
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
<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 Stats</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
|
||||
<g
|
||||
data-testid="main-card-body"
|
||||
transform="translate(0, 55)"
|
||||
>
|
||||
|
||||
<svg x="0" y="0" width="100%">
|
||||
|
||||
<mask id="rect-mask">
|
||||
<rect x="25" y="0" width="440" height="8" fill="white" rx="5" />
|
||||
</mask>
|
||||
|
||||
<rect
|
||||
mask="url(#rect-mask)"
|
||||
data-testid="lang-progress"
|
||||
x="0"
|
||||
y="0"
|
||||
width="436.4025"
|
||||
height="8"
|
||||
fill="#858585"
|
||||
/>
|
||||
|
||||
<rect
|
||||
mask="url(#rect-mask)"
|
||||
data-testid="lang-progress"
|
||||
x="436.4025"
|
||||
y="0"
|
||||
width="30.503999999999998"
|
||||
height="8"
|
||||
fill="#3178c6"
|
||||
/>
|
||||
|
||||
+76
-29
@@ -25,7 +25,7 @@ stats.rank = calculateRank({
|
||||
issues: stats.totalIssues,
|
||||
});
|
||||
|
||||
const data = {
|
||||
const data_stats = {
|
||||
data: {
|
||||
user: {
|
||||
name: stats.name,
|
||||
@@ -40,15 +40,6 @@ const data = {
|
||||
followers: { totalCount: 0 },
|
||||
repositories: {
|
||||
totalCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const repositoriesData = {
|
||||
data: {
|
||||
user: {
|
||||
repositories: {
|
||||
nodes: [{ stargazers: { totalCount: 100 } }],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
@@ -83,11 +74,7 @@ const faker = (query, data) => {
|
||||
setHeader: jest.fn(),
|
||||
send: jest.fn(),
|
||||
};
|
||||
mock
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.replyOnce(200, data)
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.replyOnce(200, repositoriesData);
|
||||
mock.onPost("https://api.github.com/graphql").replyOnce(200, data);
|
||||
|
||||
return { req, res };
|
||||
};
|
||||
@@ -98,7 +85,7 @@ afterEach(() => {
|
||||
|
||||
describe("Test /api/", () => {
|
||||
it("should test the request", async () => {
|
||||
const { req, res } = faker({}, data);
|
||||
const { req, res } = faker({}, data_stats);
|
||||
|
||||
await api(req, res);
|
||||
|
||||
@@ -133,7 +120,7 @@ describe("Test /api/", () => {
|
||||
text_color: "fff",
|
||||
bg_color: "fff",
|
||||
},
|
||||
data,
|
||||
data_stats,
|
||||
);
|
||||
|
||||
await api(req, res);
|
||||
@@ -154,23 +141,33 @@ describe("Test /api/", () => {
|
||||
});
|
||||
|
||||
it("should have proper cache", async () => {
|
||||
const { req, res } = faker({}, data);
|
||||
const { req, res } = faker({}, data_stats);
|
||||
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader.mock.calls).toEqual([
|
||||
["Content-Type", "image/svg+xml"],
|
||||
["Cache-Control", `public, max-age=${CONSTANTS.FOUR_HOURS}`],
|
||||
[
|
||||
"Cache-Control",
|
||||
`max-age=${CONSTANTS.FOUR_HOURS / 2}, s-maxage=${
|
||||
CONSTANTS.FOUR_HOURS
|
||||
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it("should set proper cache", async () => {
|
||||
const { req, res } = faker({ cache_seconds: 15000 }, data);
|
||||
const { req, res } = faker({ cache_seconds: 15000 }, data_stats);
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader.mock.calls).toEqual([
|
||||
["Content-Type", "image/svg+xml"],
|
||||
["Cache-Control", `public, max-age=${15000}`],
|
||||
[
|
||||
"Cache-Control",
|
||||
`max-age=7500, s-maxage=${15000}, stale-while-revalidate=${
|
||||
CONSTANTS.ONE_DAY
|
||||
}`,
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -180,39 +177,54 @@ describe("Test /api/", () => {
|
||||
|
||||
expect(res.setHeader.mock.calls).toEqual([
|
||||
["Content-Type", "image/svg+xml"],
|
||||
["Cache-Control", `no-store`],
|
||||
["Cache-Control", `no-cache, no-store, must-revalidate`],
|
||||
]);
|
||||
});
|
||||
|
||||
it("should set proper cache with clamped values", async () => {
|
||||
{
|
||||
let { req, res } = faker({ cache_seconds: 200000 }, data);
|
||||
let { req, res } = faker({ cache_seconds: 200000 }, data_stats);
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader.mock.calls).toEqual([
|
||||
["Content-Type", "image/svg+xml"],
|
||||
["Cache-Control", `public, max-age=${CONSTANTS.ONE_DAY}`],
|
||||
[
|
||||
"Cache-Control",
|
||||
`max-age=${CONSTANTS.ONE_DAY / 2}, s-maxage=${
|
||||
CONSTANTS.ONE_DAY
|
||||
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// note i'm using block scoped vars
|
||||
{
|
||||
let { req, res } = faker({ cache_seconds: 0 }, data);
|
||||
let { req, res } = faker({ cache_seconds: 0 }, data_stats);
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader.mock.calls).toEqual([
|
||||
["Content-Type", "image/svg+xml"],
|
||||
["Cache-Control", `public, max-age=${CONSTANTS.FOUR_HOURS}`],
|
||||
[
|
||||
"Cache-Control",
|
||||
`max-age=${CONSTANTS.FOUR_HOURS / 2}, s-maxage=${
|
||||
CONSTANTS.FOUR_HOURS
|
||||
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
{
|
||||
let { req, res } = faker({ cache_seconds: -10000 }, data);
|
||||
let { req, res } = faker({ cache_seconds: -10000 }, data_stats);
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader.mock.calls).toEqual([
|
||||
["Content-Type", "image/svg+xml"],
|
||||
["Cache-Control", `public, max-age=${CONSTANTS.FOUR_HOURS}`],
|
||||
[
|
||||
"Cache-Control",
|
||||
`max-age=${CONSTANTS.FOUR_HOURS / 2}, s-maxage=${
|
||||
CONSTANTS.FOUR_HOURS
|
||||
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
|
||||
],
|
||||
]);
|
||||
}
|
||||
});
|
||||
@@ -223,7 +235,7 @@ describe("Test /api/", () => {
|
||||
username: "anuraghazra",
|
||||
count_private: true,
|
||||
},
|
||||
data,
|
||||
data_stats,
|
||||
);
|
||||
|
||||
await api(req, res);
|
||||
@@ -248,4 +260,39 @@ describe("Test /api/", () => {
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow changing ring_color", async () => {
|
||||
const { req, res } = faker(
|
||||
{
|
||||
username: "anuraghazra",
|
||||
hide: "issues,prs,contribs",
|
||||
show_icons: true,
|
||||
hide_border: true,
|
||||
line_height: 100,
|
||||
title_color: "fff",
|
||||
ring_color: "0000ff",
|
||||
icon_color: "fff",
|
||||
text_color: "fff",
|
||||
bg_color: "fff",
|
||||
},
|
||||
data_stats,
|
||||
);
|
||||
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
|
||||
expect(res.send).toBeCalledWith(
|
||||
renderStatsCard(stats, {
|
||||
hide: ["issues", "prs", "contribs"],
|
||||
show_icons: true,
|
||||
hide_border: true,
|
||||
line_height: 100,
|
||||
title_color: "fff",
|
||||
ring_color: "0000ff",
|
||||
icon_color: "fff",
|
||||
text_color: "fff",
|
||||
bg_color: "fff",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,6 @@ describe("Test calculateRank", () => {
|
||||
prs: 300,
|
||||
issues: 200,
|
||||
}),
|
||||
).toStrictEqual({ level: "A+", score: 49.16605417270399 });
|
||||
).toStrictEqual({ level: "A+", score: 49.25629684876535 });
|
||||
});
|
||||
});
|
||||
|
||||
+20
-25
@@ -11,27 +11,22 @@ import { renderStatsCard } from "../../src/cards/stats-card.js";
|
||||
import { renderTopLanguages } from "../../src/cards/top-languages-card.js";
|
||||
import { renderWakatimeCard } from "../../src/cards/wakatime-card.js";
|
||||
|
||||
const REPO = "dummy-cra";
|
||||
const USER = "grsdummy";
|
||||
const REPO = "curly-fiesta";
|
||||
const USER = "catelinemnemosyne";
|
||||
const STATS_DATA = {
|
||||
name: "grsdummy",
|
||||
totalPRs: 2,
|
||||
totalCommits: 2,
|
||||
name: "Cateline Mnemosyne",
|
||||
totalPRs: 1,
|
||||
totalCommits: 7,
|
||||
totalIssues: 1,
|
||||
totalStars: 1,
|
||||
contributedTo: 2,
|
||||
contributedTo: 1,
|
||||
rank: {
|
||||
level: "A+",
|
||||
score: 51.01013099671447,
|
||||
score: 50.893750297869225,
|
||||
},
|
||||
};
|
||||
|
||||
const LANGS_DATA = {
|
||||
TypeScript: {
|
||||
color: "#3178c6",
|
||||
name: "TypeScript",
|
||||
size: 2049,
|
||||
},
|
||||
HTML: {
|
||||
color: "#e34c26",
|
||||
name: "HTML",
|
||||
@@ -42,19 +37,19 @@ const LANGS_DATA = {
|
||||
name: "CSS",
|
||||
size: 930,
|
||||
},
|
||||
Python: {
|
||||
color: "#3572A5",
|
||||
name: "Python",
|
||||
size: 671,
|
||||
JavaScript: {
|
||||
color: "#f1e05a",
|
||||
name: "JavaScript",
|
||||
size: 1912,
|
||||
},
|
||||
};
|
||||
|
||||
const WAKATIME_DATA = {
|
||||
human_readable_range: "last week",
|
||||
is_already_updating: false,
|
||||
is_coding_activity_visible: false,
|
||||
is_coding_activity_visible: true,
|
||||
is_including_today: false,
|
||||
is_other_usage_visible: false,
|
||||
is_other_usage_visible: true,
|
||||
is_stuck: false,
|
||||
is_up_to_date: false,
|
||||
is_up_to_date_pending_future: false,
|
||||
@@ -62,24 +57,24 @@ const WAKATIME_DATA = {
|
||||
range: "last_7_days",
|
||||
status: "pending_update",
|
||||
timeout: 15,
|
||||
username: "grsdummy",
|
||||
username: USER,
|
||||
writes_only: false,
|
||||
};
|
||||
|
||||
const REPOSITORY_DATA = {
|
||||
name: "dummy-cra",
|
||||
nameWithOwner: "grsdummy/dummy-cra",
|
||||
name: REPO,
|
||||
nameWithOwner: `${USER}/cra-test`,
|
||||
isPrivate: false,
|
||||
isArchived: false,
|
||||
isTemplate: false,
|
||||
stargazers: {
|
||||
totalCount: 1,
|
||||
},
|
||||
description: "Dummy create react app.",
|
||||
description: "Simple cra test repo.",
|
||||
primaryLanguage: {
|
||||
color: "#3178c6",
|
||||
id: "MDg6TGFuZ3VhZ2UyODc=",
|
||||
name: "TypeScript",
|
||||
color: "#f1e05a",
|
||||
id: "MDg6TGFuZ3VhZ2UxNDA=",
|
||||
name: "JavaScript",
|
||||
},
|
||||
forkCount: 0,
|
||||
starCount: 1,
|
||||
|
||||
+92
-34
@@ -4,7 +4,8 @@ import MockAdapter from "axios-mock-adapter";
|
||||
import { calculateRank } from "../src/calculateRank.js";
|
||||
import { fetchStats } from "../src/fetchers/stats-fetcher.js";
|
||||
|
||||
const data = {
|
||||
// Test parameters.
|
||||
const data_stats = {
|
||||
data: {
|
||||
user: {
|
||||
name: "Anurag Hazra",
|
||||
@@ -19,15 +20,6 @@ const data = {
|
||||
followers: { totalCount: 100 },
|
||||
repositories: {
|
||||
totalCount: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const firstRepositoriesData = {
|
||||
data: {
|
||||
user: {
|
||||
repositories: {
|
||||
nodes: [
|
||||
{ name: "test-repo-1", stargazers: { totalCount: 100 } },
|
||||
{ name: "test-repo-2", stargazers: { totalCount: 100 } },
|
||||
@@ -42,7 +34,7 @@ const firstRepositoriesData = {
|
||||
},
|
||||
};
|
||||
|
||||
const secondRepositoriesData = {
|
||||
const data_repo = {
|
||||
data: {
|
||||
user: {
|
||||
repositories: {
|
||||
@@ -59,7 +51,7 @@ const secondRepositoriesData = {
|
||||
},
|
||||
};
|
||||
|
||||
const repositoriesWithZeroStarsData = {
|
||||
const data_repo_zero_stars = {
|
||||
data: {
|
||||
user: {
|
||||
repositories: {
|
||||
@@ -93,13 +85,12 @@ const error = {
|
||||
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)
|
||||
.replyOnce(200, data_stats)
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.replyOnce(200, firstRepositoriesData);
|
||||
// .onPost("https://api.github.com/graphql") // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
// .replyOnce(200, secondRepositoriesData); // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
.replyOnce(200, data_repo);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -114,8 +105,7 @@ describe("Test fetchStats", () => {
|
||||
totalRepos: 5,
|
||||
followers: 100,
|
||||
contributions: 61,
|
||||
// stargazers: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
stargazers: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
stargazers: 300,
|
||||
prs: 300,
|
||||
issues: 200,
|
||||
});
|
||||
@@ -126,8 +116,7 @@ describe("Test fetchStats", () => {
|
||||
totalCommits: 100,
|
||||
totalIssues: 200,
|
||||
totalPRs: 300,
|
||||
// totalStars: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
totalStars: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
totalStars: 300,
|
||||
rank,
|
||||
});
|
||||
});
|
||||
@@ -136,9 +125,9 @@ describe("Test fetchStats", () => {
|
||||
mock.reset();
|
||||
mock
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.replyOnce(200, data)
|
||||
.replyOnce(200, data_stats)
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.replyOnce(200, repositoriesWithZeroStarsData);
|
||||
.replyOnce(200, data_repo_zero_stars);
|
||||
|
||||
let stats = await fetchStats("anuraghazra");
|
||||
const rank = calculateRank({
|
||||
@@ -178,8 +167,7 @@ describe("Test fetchStats", () => {
|
||||
totalRepos: 5,
|
||||
followers: 100,
|
||||
contributions: 61,
|
||||
// stargazers: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
stargazers: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
stargazers: 300,
|
||||
prs: 300,
|
||||
issues: 200,
|
||||
});
|
||||
@@ -190,8 +178,7 @@ describe("Test fetchStats", () => {
|
||||
totalCommits: 150,
|
||||
totalIssues: 200,
|
||||
totalPRs: 300,
|
||||
// totalStars: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
totalStars: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
totalStars: 300,
|
||||
rank,
|
||||
});
|
||||
});
|
||||
@@ -207,8 +194,7 @@ describe("Test fetchStats", () => {
|
||||
totalRepos: 5,
|
||||
followers: 100,
|
||||
contributions: 61,
|
||||
// stargazers: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
stargazers: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
stargazers: 300,
|
||||
prs: 300,
|
||||
issues: 200,
|
||||
});
|
||||
@@ -219,8 +205,7 @@ describe("Test fetchStats", () => {
|
||||
totalCommits: 1050,
|
||||
totalIssues: 200,
|
||||
totalPRs: 300,
|
||||
// totalStars: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
totalStars: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
totalStars: 300,
|
||||
rank,
|
||||
});
|
||||
});
|
||||
@@ -236,8 +221,7 @@ describe("Test fetchStats", () => {
|
||||
totalRepos: 5,
|
||||
followers: 100,
|
||||
contributions: 61,
|
||||
// stargazers: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
stargazers: 200, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
stargazers: 200,
|
||||
prs: 300,
|
||||
issues: 200,
|
||||
});
|
||||
@@ -248,8 +232,82 @@ describe("Test fetchStats", () => {
|
||||
totalCommits: 1050,
|
||||
totalIssues: 200,
|
||||
totalPRs: 300,
|
||||
// totalStars: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
totalStars: 200, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
|
||||
totalStars: 200,
|
||||
rank,
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch two pages of stars if 'FETCH_MULTI_PAGE_STARS' env variable is set to `true`", async () => {
|
||||
process.env.FETCH_MULTI_PAGE_STARS = true;
|
||||
|
||||
let stats = await fetchStats("anuraghazra");
|
||||
const rank = calculateRank({
|
||||
totalCommits: 100,
|
||||
totalRepos: 5,
|
||||
followers: 100,
|
||||
contributions: 61,
|
||||
stargazers: 400,
|
||||
prs: 300,
|
||||
issues: 200,
|
||||
});
|
||||
|
||||
expect(stats).toStrictEqual({
|
||||
contributedTo: 61,
|
||||
name: "Anurag Hazra",
|
||||
totalCommits: 100,
|
||||
totalIssues: 200,
|
||||
totalPRs: 300,
|
||||
totalStars: 400,
|
||||
rank,
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch one page of stars if 'FETCH_MULTI_PAGE_STARS' env variable is set to `false`", async () => {
|
||||
process.env.FETCH_MULTI_PAGE_STARS = "false";
|
||||
|
||||
let stats = await fetchStats("anuraghazra");
|
||||
const rank = calculateRank({
|
||||
totalCommits: 100,
|
||||
totalRepos: 5,
|
||||
followers: 100,
|
||||
contributions: 61,
|
||||
stargazers: 300,
|
||||
prs: 300,
|
||||
issues: 200,
|
||||
});
|
||||
|
||||
expect(stats).toStrictEqual({
|
||||
contributedTo: 61,
|
||||
name: "Anurag Hazra",
|
||||
totalCommits: 100,
|
||||
totalIssues: 200,
|
||||
totalPRs: 300,
|
||||
totalStars: 300,
|
||||
rank,
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch one page of stars if 'FETCH_MULTI_PAGE_STARS' env variable is not set", async () => {
|
||||
process.env.FETCH_MULTI_PAGE_STARS = undefined;
|
||||
|
||||
let stats = await fetchStats("anuraghazra");
|
||||
const rank = calculateRank({
|
||||
totalCommits: 100,
|
||||
totalRepos: 5,
|
||||
followers: 100,
|
||||
contributions: 61,
|
||||
stargazers: 300,
|
||||
prs: 300,
|
||||
issues: 200,
|
||||
});
|
||||
|
||||
expect(stats).toStrictEqual({
|
||||
contributedTo: 61,
|
||||
name: "Anurag Hazra",
|
||||
totalCommits: 100,
|
||||
totalIssues: 200,
|
||||
totalPRs: 300,
|
||||
totalStars: 300,
|
||||
rank,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -78,16 +78,17 @@ describe("Test renderStatsCard", () => {
|
||||
|
||||
it("should render with custom width set", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats);
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "495");
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "450");
|
||||
|
||||
document.body.innerHTML = renderStatsCard(stats, { card_width: 400 });
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "400");
|
||||
document.body.innerHTML = renderStatsCard(stats, { card_width: 500 });
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "500");
|
||||
});
|
||||
|
||||
it("should render with custom width set and limit minimum width", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, { card_width: 1 });
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "340");
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "420");
|
||||
|
||||
// Test default minimum card width without rank circle.
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
card_width: 1,
|
||||
hide_rank: true,
|
||||
@@ -97,6 +98,7 @@ describe("Test renderStatsCard", () => {
|
||||
"305.81250000000006",
|
||||
);
|
||||
|
||||
// Test minimum card width with rank and icons.
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
card_width: 1,
|
||||
hide_rank: true,
|
||||
@@ -104,22 +106,24 @@ describe("Test renderStatsCard", () => {
|
||||
});
|
||||
expect(document.querySelector("svg")).toHaveAttribute(
|
||||
"width",
|
||||
"305.81250000000006",
|
||||
"322.81250000000006",
|
||||
);
|
||||
|
||||
// Test minimum card width with icons but without rank.
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
card_width: 1,
|
||||
hide_rank: false,
|
||||
show_icons: true,
|
||||
});
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "356");
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "437");
|
||||
|
||||
// Test minimum card width without icons or rank.
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
card_width: 1,
|
||||
hide_rank: false,
|
||||
show_icons: false,
|
||||
});
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "340");
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "420");
|
||||
});
|
||||
|
||||
it("should render default colors properly", () => {
|
||||
@@ -239,6 +243,39 @@ describe("Test renderStatsCard", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should render custom ring_color properly", () => {
|
||||
const customColors = {
|
||||
title_color: "5a0",
|
||||
ring_color: "0000ff",
|
||||
icon_color: "1b998b",
|
||||
text_color: "9991",
|
||||
bg_color: "252525",
|
||||
};
|
||||
|
||||
document.body.innerHTML = renderStatsCard(stats, { ...customColors });
|
||||
|
||||
const styleTag = document.querySelector("style");
|
||||
const stylesObject = cssToObject(styleTag.innerHTML);
|
||||
|
||||
const headerClassStyles = stylesObject[":host"][".header "];
|
||||
const statClassStyles = stylesObject[":host"][".stat "];
|
||||
const iconClassStyles = stylesObject[":host"][".icon "];
|
||||
const rankCircleStyles = stylesObject[":host"][".rank-circle "];
|
||||
const rankCircleRimStyles = stylesObject[":host"][".rank-circle-rim "];
|
||||
|
||||
expect(headerClassStyles.fill.trim()).toBe(`#${customColors.title_color}`);
|
||||
expect(statClassStyles.fill.trim()).toBe(`#${customColors.text_color}`);
|
||||
expect(iconClassStyles.fill.trim()).toBe(`#${customColors.icon_color}`);
|
||||
expect(rankCircleStyles.stroke.trim()).toBe(`#${customColors.ring_color}`);
|
||||
expect(rankCircleRimStyles.stroke.trim()).toBe(
|
||||
`#${customColors.ring_color}`,
|
||||
);
|
||||
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
|
||||
"fill",
|
||||
"#252525",
|
||||
);
|
||||
});
|
||||
|
||||
it("should render icons correctly", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
show_icons: true,
|
||||
@@ -279,7 +316,7 @@ describe("Test renderStatsCard", () => {
|
||||
|
||||
expect(
|
||||
document.body.getElementsByTagName("svg")[0].getAttribute("width"),
|
||||
).toBe("270");
|
||||
).toBe("287");
|
||||
});
|
||||
|
||||
it("should render translations", () => {
|
||||
@@ -296,7 +333,7 @@ describe("Test renderStatsCard", () => {
|
||||
document.querySelector(
|
||||
'g[transform="translate(0, 25)"]>.stagger>.stat.bold',
|
||||
).textContent,
|
||||
).toMatchInlineSnapshot(`"累计提交数(commit) (2022):"`);
|
||||
).toMatchInlineSnapshot(`"累计提交数(commit) (2023):"`);
|
||||
expect(
|
||||
document.querySelector(
|
||||
'g[transform="translate(0, 50)"]>.stagger>.stat.bold',
|
||||
@@ -311,7 +348,7 @@ describe("Test renderStatsCard", () => {
|
||||
document.querySelector(
|
||||
'g[transform="translate(0, 100)"]>.stagger>.stat.bold',
|
||||
).textContent,
|
||||
).toMatchInlineSnapshot(`"参与项目数:"`);
|
||||
).toMatchInlineSnapshot(`"参与项目数 (last year):"`);
|
||||
});
|
||||
|
||||
it("should render without rounding", () => {
|
||||
|
||||
@@ -16,6 +16,15 @@ describe("Test Render Wakatime Card", () => {
|
||||
expect(card).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render correctly with compact layout when langs_count is set", () => {
|
||||
const card = renderWakatimeCard(wakaTimeData.data, {
|
||||
layout: "compact",
|
||||
langs_count: 2,
|
||||
});
|
||||
|
||||
expect(card).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should hide languages when hide is passed", () => {
|
||||
document.body.innerHTML = renderWakatimeCard(wakaTimeData.data, {
|
||||
hide: ["YAML", "Other"],
|
||||
|
||||
@@ -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"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
encodeHTML,
|
||||
getCardColors,
|
||||
kFormatter,
|
||||
parseBoolean,
|
||||
renderError,
|
||||
wrapTextMultiline,
|
||||
} from "../src/common/utils.js";
|
||||
@@ -19,6 +20,23 @@ describe("Test utils.js", () => {
|
||||
expect(kFormatter(9900000)).toBe("9900k");
|
||||
});
|
||||
|
||||
it("should test parseBoolean", () => {
|
||||
expect(parseBoolean(true)).toBe(true);
|
||||
expect(parseBoolean(false)).toBe(false);
|
||||
|
||||
expect(parseBoolean("true")).toBe(true);
|
||||
expect(parseBoolean("false")).toBe(false);
|
||||
expect(parseBoolean("True")).toBe(true);
|
||||
expect(parseBoolean("False")).toBe(false);
|
||||
expect(parseBoolean("TRUE")).toBe(true);
|
||||
expect(parseBoolean("FALSE")).toBe(false);
|
||||
|
||||
expect(parseBoolean("1")).toBe(undefined);
|
||||
expect(parseBoolean("0")).toBe(undefined);
|
||||
expect(parseBoolean("")).toBe(undefined);
|
||||
expect(parseBoolean(undefined)).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should test encodeHTML", () => {
|
||||
expect(encodeHTML(`<html>hello world<,.#4^&^@%!))`)).toBe(
|
||||
"<html>hello world<,.#4^&^@%!))",
|
||||
@@ -48,6 +66,7 @@ describe("Test utils.js", () => {
|
||||
let colors = getCardColors({
|
||||
title_color: "f00",
|
||||
text_color: "0f0",
|
||||
ring_color: "0000ff",
|
||||
icon_color: "00f",
|
||||
bg_color: "fff",
|
||||
border_color: "fff",
|
||||
@@ -57,6 +76,7 @@ describe("Test utils.js", () => {
|
||||
titleColor: "#f00",
|
||||
textColor: "#0f0",
|
||||
iconColor: "#00f",
|
||||
ringColor: "#0000ff",
|
||||
bgColor: "#fff",
|
||||
borderColor: "#fff",
|
||||
});
|
||||
@@ -75,6 +95,7 @@ describe("Test utils.js", () => {
|
||||
titleColor: "#2f80ed",
|
||||
textColor: "#0f0",
|
||||
iconColor: "#00f",
|
||||
ringColor: "#2f80ed",
|
||||
bgColor: "#fff",
|
||||
borderColor: "#e4e2e2",
|
||||
});
|
||||
@@ -87,11 +108,31 @@ describe("Test utils.js", () => {
|
||||
expect(colors).toStrictEqual({
|
||||
titleColor: "#fff",
|
||||
textColor: "#9f9f9f",
|
||||
ringColor: "#fff",
|
||||
iconColor: "#79ff97",
|
||||
bgColor: "#151515",
|
||||
borderColor: "#e4e2e2",
|
||||
});
|
||||
});
|
||||
|
||||
it("getCardColors: should return ring color equal to title color if not ring color is defined", () => {
|
||||
let colors = getCardColors({
|
||||
title_color: "f00",
|
||||
text_color: "0f0",
|
||||
icon_color: "00f",
|
||||
bg_color: "fff",
|
||||
border_color: "fff",
|
||||
theme: "dark",
|
||||
});
|
||||
expect(colors).toStrictEqual({
|
||||
titleColor: "#f00",
|
||||
textColor: "#0f0",
|
||||
iconColor: "#00f",
|
||||
ringColor: "#f00",
|
||||
bgColor: "#fff",
|
||||
borderColor: "#fff",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("wrapTextMultiline", () => {
|
||||
|
||||
@@ -367,6 +367,20 @@ export const themes = {
|
||||
text_color: "e0def4",
|
||||
bg_color: "191724",
|
||||
},
|
||||
date_night: {
|
||||
title_color: "DA7885",
|
||||
text_color: "E1B2A2",
|
||||
icon_color: "BB8470",
|
||||
border_color: "170F0C",
|
||||
bg_color: "170F0C",
|
||||
},
|
||||
rose: {
|
||||
title_color: "8d192b",
|
||||
text_color: "862931",
|
||||
icon_color: "B71F36",
|
||||
border_color: "e9d8d4",
|
||||
bg_color: "e9d8d4",
|
||||
},
|
||||
};
|
||||
|
||||
export default themes;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"functions": {
|
||||
"api/*.js": {
|
||||
"memory": 128,
|
||||
"maxDuration": 30
|
||||
"maxDuration": 10
|
||||
}
|
||||
},
|
||||
"redirects": [
|
||||
|
||||
Reference in New Issue
Block a user