Compare commits

..
Author SHA1 Message Date
Rick Staa c504c9338e my quick tests for implementing stale-if-error 2023-01-22 11:06:57 +01:00
Rick Staa aa2e8ff3c5 feat: rate limit error chaching
Rate limit error caching to alleviate PATs.
2023-01-21 18:50:37 +01:00
47 changed files with 204 additions and 3395 deletions
-10
View File
@@ -1,10 +0,0 @@
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()
-20
View File
@@ -1,20 +0,0 @@
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"
-1
View File
@@ -5,7 +5,6 @@ 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,7 +8,6 @@ on:
jobs:
closeEmptyIssuesAndTemplates:
if: github.repository == 'anuraghazra/github-readme-stats'
name: Close empty issues
runs-on: ubuntu-latest
steps:
-1
View File
@@ -4,7 +4,6 @@ on:
jobs:
triage:
if: github.repository == 'anuraghazra/github-readme-stats'
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
@@ -5,7 +5,6 @@ on:
jobs:
closeOldThemePrs:
if: github.repository == 'anuraghazra/github-readme-stats'
name: Close stale 'invalid' theme PRs
runs-on: ubuntu-latest
strategy:
@@ -5,7 +5,6 @@ on:
jobs:
showAndLabelTopIssues:
if: github.repository == 'anuraghazra/github-readme-stats'
name: Update top issues Dashboard.
runs-on: ubuntu-latest
steps:
+25 -3
View File
@@ -7,6 +7,7 @@ import {
parseBoolean,
renderError,
} from "../src/common/utils.js";
import { HttpException } from "../src/common/exceptions.js";
import { fetchStats } from "../src/fetchers/stats-fetcher.js";
import { isLocaleAvailable } from "../src/translations.js";
@@ -56,7 +57,7 @@ export default async (req, res) => {
);
const cacheSeconds = clampValue(
parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10),
parseInt(cache_seconds || CONSTANTS.CARD_CACHE_SECONDS, 10),
CONSTANTS.FOUR_HOURS,
CONSTANTS.ONE_DAY,
);
@@ -65,7 +66,8 @@ export default async (req, res) => {
"Cache-Control",
`max-age=${
cacheSeconds / 2
}, s-maxage=${cacheSeconds}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
}, s-maxage=${cacheSeconds}, stale-while-revalidate=${CONSTANTS.ONE_DAY},
stale-if-error=${CONSTANTS.ONE_HOUR}`,
);
return res.send(
@@ -93,7 +95,27 @@ export default async (req, res) => {
}),
);
} catch (err) {
res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses.
// Throw error if REST and GraphQL API calls fail. This way we can return a cached
if (err instanceof HttpException) {
// throw err;
// throw new Error(err.message);
// return res.status(404).;
// return {statusCode: 404, body: err.message}
return res
.status(500)
.send(
renderError(err.errors[0].message, err.errors[0].secondaryMessage),
);
}
// Cache the error response less frequently.
res.setHeader(
"Cache-Control",
`max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${
CONSTANTS.ERROR_CACHE_SECONDS
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}
stale-if-error=${CONSTANTS.ONE_HOUR}`,
);
return res.send(renderError(err.message, err.secondaryMessage));
}
};
+7 -2
View File
@@ -40,7 +40,7 @@ export default async (req, res) => {
const repoData = await fetchRepo(username, repo);
let cacheSeconds = clampValue(
parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10),
parseInt(cache_seconds || CONSTANTS.CARD_CACHE_SECONDS, 10),
CONSTANTS.FOUR_HOURS,
CONSTANTS.ONE_DAY,
);
@@ -80,7 +80,12 @@ export default async (req, res) => {
}),
);
} catch (err) {
res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses.
res.setHeader(
"Cache-Control",
`max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${
CONSTANTS.ERROR_CACHE_SECONDS
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
); // Cache the error response less frequently.
return res.send(renderError(err.message, err.secondaryMessage));
}
};
-139
View File
@@ -1,139 +0,0 @@
/**
* @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);
}
};
-103
View File
@@ -1,103 +0,0 @@
/**
* @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);
}
};
+7 -2
View File
@@ -48,7 +48,7 @@ export default async (req, res) => {
);
const cacheSeconds = clampValue(
parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10),
parseInt(cache_seconds || CONSTANTS.CARD_CACHE_SECONDS, 10),
CONSTANTS.FOUR_HOURS,
CONSTANTS.ONE_DAY,
);
@@ -80,7 +80,12 @@ export default async (req, res) => {
}),
);
} catch (err) {
res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses.
res.setHeader(
"Cache-Control",
`max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${
CONSTANTS.ERROR_CACHE_SECONDS
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
); // Cache the error response less frequently.
return res.send(renderError(err.message, err.secondaryMessage));
}
};
+7 -6
View File
@@ -43,15 +43,11 @@ export default async (req, res) => {
const stats = await fetchWakatimeStats({ username, api_domain, range });
let cacheSeconds = clampValue(
parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10),
parseInt(cache_seconds || CONSTANTS.CARD_CACHE_SECONDS, 10),
CONSTANTS.FOUR_HOURS,
CONSTANTS.ONE_DAY,
);
if (!cache_seconds) {
cacheSeconds = CONSTANTS.FOUR_HOURS;
}
res.setHeader(
"Cache-Control",
`max-age=${
@@ -80,7 +76,12 @@ export default async (req, res) => {
}),
);
} catch (err) {
res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses.
res.setHeader(
"Cache-Control",
`max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${
CONSTANTS.ERROR_CACHE_SECONDS
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
); // Cache the error response less frequently.
return res.send(renderError(err.message, err.secondaryMessage));
}
};
+1 -1
View File
@@ -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.21.2",
"axios-mock-adapter": "^1.18.1",
"color-contrast-checker": "^2.1.0",
"hjson": "^3.2.2",
"husky": "^8.0.0",
+2 -3
View File
@@ -39,18 +39,17 @@
"@testing-library/dom": "^8.17.1",
"@testing-library/jest-dom": "^5.16.5",
"@uppercod/css-to-object": "^1.1.1",
"axios-mock-adapter": "^1.21.2",
"axios-mock-adapter": "^1.18.1",
"color-contrast-checker": "^2.1.0",
"hjson": "^3.2.2",
"husky": "^8.0.0",
"jest": "^29.0.3",
"jest-environment-jsdom": "^29.0.3",
"jest-svg-snapshot": "^0.1.0",
"js-yaml": "^4.1.0",
"lint-staged": "^13.0.3",
"lodash.snakecase": "^4.1.1",
"parse-diff": "^0.7.0",
"prettier": "^2.8.3"
"prettier": "^2.1.2"
},
"dependencies": {
"axios": "^0.24.0",
+11 -25
View File
@@ -12,11 +12,6 @@ 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.
*
@@ -223,17 +218,11 @@ 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 + /* 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;
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;
let width = isNaN(card_width) ? defaultCardWidth : card_width;
if (width < minCardWidth) {
width = minCardWidth;
@@ -262,21 +251,18 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
/**
* Calculates the right rank circle translation values such that the rank circle
* keeps respecting the following padding:
* keeps respecting the padding.
*
* 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.
* width > 450: The default left padding of 50 px will be used.
* width < 450: The left and right padding will shrink equally.
*
* @returns {number} - Rank circle translation value.
*/
const calculateRankXTranslation = () => {
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;
if (width < 450) {
return width - 95 + (45 * (450 - 340)) / 110;
} else {
return minXTranslation + (width - minCardWidth) / 2;
return width - 95;
}
};
+1 -5
View File
@@ -177,11 +177,7 @@ class Card {
}
${this.css}
${
process.env.NODE_ENV === "test" || this.animations === false
? ""
: getAnimations()
}
${process.env.NODE_ENV === "test" ? "" : getAnimations()}
${
this.animations === false
? `* { animation-duration: 0s !important; animation-delay: 0s !important; }`
+82
View File
@@ -0,0 +1,82 @@
/**
* @file GRS Exception/Errors.
*/
const SECONDARY_ERROR_MESSAGES = {
MAX_RETRY:
"Please add an env variable called PAT_1 with your github token in vercel",
USER_NOT_FOUND: "Make sure the provided username is not an organization",
GRAPHQL_ERROR: "Please try again later",
};
/**
* Custom error class to handle custom GRS errors.
*
* @extends Error
*/
class CustomError extends Error {
/**
* @param {string} message Error message.
* @param {string} type Error type.
*/
constructor(message, type) {
super(message);
this.type = type;
this.secondaryMessage = SECONDARY_ERROR_MESSAGES[type] || type;
}
static MAX_RETRY = "MAX_RETRY";
static USER_NOT_FOUND = "USER_NOT_FOUND";
static GRAPHQL_ERROR = "GRAPHQL_ERROR";
}
/**
* Missing query parameter class.
*
* @extends Error
*/
class MissingParamError extends Error {
/**
* @param {string[]} missedParams
* @param {string?=} secondaryMessage
*/
constructor(missedParams, secondaryMessage) {
const msg = `Missing params ${missedParams
.map((p) => `"${p}"`)
.join(", ")} make sure you pass the parameters in URL`;
super(msg);
this.missedParams = missedParams;
this.secondaryMessage = secondaryMessage;
}
}
/**
* HttpException class.
*
* @extends Error
*/
class HttpException extends Error {
/**
* Create a HttpException.
*
* @param {number} statusCode - The status code.
* @param {string} message - The error message.
* @param {string[]} errors - The errors.
*/
constructor(
statusCode,
message = "Exception occurred during the processing of HTTP requests.",
errors = [],
) {
super(message);
this.statusCode = statusCode;
this.errors = errors;
}
}
export {
SECONDARY_ERROR_MESSAGES,
CustomError,
MissingParamError,
HttpException,
};
+1 -2
View File
@@ -21,10 +21,9 @@ export {
wrapTextMultiline,
logger,
CONSTANTS,
CustomError,
MissingParamError,
measureText,
lowercaseTrim,
chunkArray,
parseEmojis,
} from "./utils.js";
export { CustomError, MissingParamError, HttpException } from "./exceptions.js";
+13 -4
View File
@@ -1,4 +1,8 @@
import { CustomError, logger } from "./utils.js";
import { logger } from "./utils.js";
import {
CustomError,
HttpException,
} from "./exceptions.js";
// Script variables.
const PATs = Object.keys(process.env).filter((key) =>
@@ -16,8 +20,13 @@ const RETRIES = PATs ? PATs : 7;
* @returns Promise<retryer>
*/
const retryer = async (fetcher, variables, retries = 0) => {
if (retries > RETRIES) {
throw new CustomError("Maximum retries exceeded", CustomError.MAX_RETRY);
// if (retries > RETRIES) {
if (true) { // FIXME: Test out error
throw new HttpException(
statusCode=500,
message=`Max GraphQL retries exceeded. Please add an env variable called PAT_1 with your github token in vercel.`,
errors=[new CustomError("Maximum retries exceeded", CustomError.MAX_RETRY)]
);
}
try {
// try to fetch with the first token since RETRIES is 0 index i'm adding +1
@@ -60,5 +69,5 @@ const retryer = async (fetcher, variables, retries = 0) => {
}
};
export { retryer };
export { retryer, RETRIES };
export default retryer;
+12 -63
View File
@@ -22,8 +22,7 @@ const renderError = (message, secondaryMessage = "") => {
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #252525 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="${
ERROR_CARD_LENGTH - 1
<rect x="0.5" y="0.5" width="${ERROR_CARD_LENGTH - 1
}" height="99%" rx="4.5" fill="#FFFEFE" stroke="#E4E2E2"/>
<text x="25" y="45" class="text">Something went wrong! file an issue at https://tiny.one/readme-stats</text>
<text data-testid="message" x="25" y="55" class="text small">
@@ -288,62 +287,28 @@ const wrapTextMultiline = (text, width = 59, maxLines = 3) => {
return multiLineText;
};
const noop = () => {};
const noop = () => { };
// return console instance based on the environment
const logger =
process.env.NODE_ENV !== "test" ? console : { log: noop, error: noop };
// Cache settings.
const CARD_CACHE_SECONDS = 14400;
const ERROR_CACHE_SECONDS = 600;
const CONSTANTS = {
ONE_MINUTE: 60,
FIVE_MINUTES: 300,
TEN_MINUTES: 600,
FIFTEEN_MINUTES: 900,
THIRTY_MINUTES: 1800,
TWO_HOURS: 7200,
FOUR_HOURS: 14400,
ONE_DAY: 86400,
CARD_CACHE_SECONDS: CARD_CACHE_SECONDS,
ERROR_CACHE_SECONDS: ERROR_CACHE_SECONDS,
};
const SECONDARY_ERROR_MESSAGES = {
MAX_RETRY:
"Please add an env variable called PAT_1 with your github token in vercel",
USER_NOT_FOUND: "Make sure the provided username is not an organization",
GRAPHQL_ERROR: "Please try again later",
};
/**
* Custom error class to handle custom GRS errors.
*/
class CustomError extends Error {
/**
* @param {string} message Error message.
* @param {string} type Error type.
*/
constructor(message, type) {
super(message);
this.type = type;
this.secondaryMessage = SECONDARY_ERROR_MESSAGES[type] || type;
}
static MAX_RETRY = "MAX_RETRY";
static USER_NOT_FOUND = "USER_NOT_FOUND";
static GRAPHQL_ERROR = "GRAPHQL_ERROR";
}
/**
* Missing query parameter class.
*/
class MissingParamError extends Error {
/**
* @param {string[]} missedParams
* @param {string?=} secondaryMessage
*/
constructor(missedParams, secondaryMessage) {
const msg = `Missing params ${missedParams
.map((p) => `"${p}"`)
.join(", ")} make sure you pass the parameters in URL`;
super(msg);
this.missedParams = missedParams;
this.secondaryMessage = secondaryMessage;
}
}
/**
* Retrieve text length.
*
@@ -424,19 +389,6 @@ const parseEmojis = (str) => {
});
};
/**
* 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,
renderError,
@@ -454,11 +406,8 @@ export {
wrapTextMultiline,
logger,
CONSTANTS,
CustomError,
MissingParamError,
measureText,
lowercaseTrim,
chunkArray,
parseEmojis,
dateDiff,
};
+2 -1
View File
@@ -1,6 +1,7 @@
// @ts-check
import { retryer } from "../common/retryer.js";
import { MissingParamError, request } from "../common/utils.js";
import { request } from "../common/utils.js";
import { MissingParamError } from "../common/exceptions.js";
/**
* Repo data fetcher.
+1 -2
View File
@@ -5,12 +5,11 @@ import githubUsernameRegex from "github-username-regex";
import { calculateRank } from "../calculateRank.js";
import { retryer } from "../common/retryer.js";
import {
CustomError,
logger,
MissingParamError,
request,
wrapTextMultiline,
} from "../common/utils.js";
import { CustomError, MissingParamError } from "../common/exceptions.js";
dotenv.config();
+1 -2
View File
@@ -1,12 +1,11 @@
// @ts-check
import { retryer } from "../common/retryer.js";
import {
CustomError,
logger,
MissingParamError,
request,
wrapTextMultiline,
} from "../common/utils.js";
import { CustomError, MissingParamError } from "../common/exceptions.js";
/**
* Top languages fetcher object.
+1 -1
View File
@@ -1,5 +1,5 @@
import axios from "axios";
import { MissingParamError } from "../common/utils.js";
import { MissingParamError } from "../common/exceptions.js";
/**
* WakaTime data fetcher.
+4 -6
View File
@@ -43,11 +43,6 @@ const getProgressAnimation = ({ progress }) => {
const getAnimations = () => {
return `
/* Animations */
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
@keyframes scaleInAnimation {
from {
transform: translate(-5px, 5px) scale(0);
@@ -94,7 +89,10 @@ const getStyles = ({
/* 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: ${textColor};
animation: scaleInAnimation 0.3s ease-in-out forwards;
@@ -34,7 +34,10 @@ exports[`Test Render Wakatime Card should render correctly with compact layout 1
/* 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;
@@ -189,7 +192,10 @@ exports[`Test Render Wakatime Card should render correctly with compact layout w
/* 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;
+7 -2
View File
@@ -171,13 +171,18 @@ describe("Test /api/", () => {
]);
});
it("should not store cache when error", async () => {
it("should set shorter cache when error", async () => {
const { req, res } = faker({}, error);
await api(req, res);
expect(res.setHeader.mock.calls).toEqual([
["Content-Type", "image/svg+xml"],
["Cache-Control", `no-cache, no-store, must-revalidate`],
[
"Cache-Control",
`max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${
CONSTANTS.ERROR_CACHE_SECONDS
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
],
]);
});
-244
View File
@@ -1,244 +0,0 @@
/**
* @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"],
]);
});
});
+8 -12
View File
@@ -78,17 +78,16 @@ describe("Test renderStatsCard", () => {
it("should render with custom width set", () => {
document.body.innerHTML = renderStatsCard(stats);
expect(document.querySelector("svg")).toHaveAttribute("width", "450");
expect(document.querySelector("svg")).toHaveAttribute("width", "495");
document.body.innerHTML = renderStatsCard(stats, { card_width: 500 });
expect(document.querySelector("svg")).toHaveAttribute("width", "500");
document.body.innerHTML = renderStatsCard(stats, { card_width: 400 });
expect(document.querySelector("svg")).toHaveAttribute("width", "400");
});
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", "420");
expect(document.querySelector("svg")).toHaveAttribute("width", "340");
// Test default minimum card width without rank circle.
document.body.innerHTML = renderStatsCard(stats, {
card_width: 1,
hide_rank: true,
@@ -98,7 +97,6 @@ describe("Test renderStatsCard", () => {
"305.81250000000006",
);
// Test minimum card width with rank and icons.
document.body.innerHTML = renderStatsCard(stats, {
card_width: 1,
hide_rank: true,
@@ -106,24 +104,22 @@ describe("Test renderStatsCard", () => {
});
expect(document.querySelector("svg")).toHaveAttribute(
"width",
"322.81250000000006",
"305.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", "437");
expect(document.querySelector("svg")).toHaveAttribute("width", "356");
// 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", "420");
expect(document.querySelector("svg")).toHaveAttribute("width", "340");
});
it("should render default colors properly", () => {
@@ -316,7 +312,7 @@ describe("Test renderStatsCard", () => {
expect(
document.body.getElementsByTagName("svg")[0].getAttribute("width"),
).toBe("287");
).toBe("270");
});
it("should render translations", () => {
+2 -2
View File
@@ -1,6 +1,6 @@
import { jest } from "@jest/globals";
import "@testing-library/jest-dom";
import { retryer } from "../src/common/retryer.js";
import { retryer, RETRIES } from "../src/common/retryer.js";
import { logger } from "../src/common/utils.js";
const fetcher = jest.fn((variables, token) => {
@@ -45,7 +45,7 @@ describe("Test Retryer", () => {
try {
res = await retryer(fetcherFail, {});
} catch (err) {
expect(fetcherFail).toBeCalledTimes(8);
expect(fetcherFail).toBeCalledTimes(RETRIES + 1);
expect(err.message).toBe("Maximum retries exceeded");
}
});
-194
View File
@@ -1,194 +0,0 @@
/**
* @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"],
]);
});
});
@@ -1,177 +0,0 @@
<svg
width="800"
height="195"
viewBox="0 0 800 195"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="descId"
>
<title id="titleId">Cateline Mnemosyne's GitHub Stats, Rank: A+</title>
<desc id="descId">
Total Stars Earned: 1, Total Commits in 2023 : 1, Total PRs: 1, Total
Issues: 1, Contributed to (last year): 1
</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;
}
}
.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: #2f80ed;
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: #2f80ed;
stroke-dasharray: 250;
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
* {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
</style>
<rect
data-testid="card-bg"
x="0.5"
y="0.5"
rx="4.5"
height="99%"
stroke="#e4e2e2"
width="799"
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">
Cateline Mnemosyne's GitHub Stats
</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<g data-testid="rank-circle" transform="translate(715, 47.5)">
<circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
<circle class="rank-circle" cx="-10" cy="8" r="40" />
<g class="rank-text">
<text
x="-5"
y="3"
alignment-baseline="central"
dominant-baseline="central"
text-anchor="middle"
>
A+
</text>
</g>
</g>
<svg x="0" y="0">
<g transform="translate(0, 0)">
<g
class="stagger"
style="animation-delay: 450ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Stars Earned:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="stars">
1
</text>
</g>
</g>
<g transform="translate(0, 25)">
<g
class="stagger"
style="animation-delay: 600ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Commits (2023):</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="commits">
7
</text>
</g>
</g>
<g transform="translate(0, 50)">
<g
class="stagger"
style="animation-delay: 750ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total PRs:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="prs">1</text>
</g>
</g>
<g transform="translate(0, 75)">
<g
class="stagger"
style="animation-delay: 900ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Issues:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="issues">
1
</text>
</g>
</g>
<g transform="translate(0, 100)">
<g
class="stagger"
style="animation-delay: 1050ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Contributed to (last year):</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="contribs">
1
</text>
</g>
</g>
</svg>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

@@ -1,249 +0,0 @@
<svg
width="467"
height="195"
viewBox="0 0 467 195"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="descId"
>
<title id="titleId">Cateline Mnemosyne's GitHub Stats, Rank: A+</title>
<desc id="descId">
Total Stars Earned: 1, Total Commits in 2023 : 1, Total PRs: 1, Total
Issues: 1, Contributed to (last year): 1
</desc>
<style>
.header {
font: 600 18px "Segoe UI", Ubuntu, Sans-Serif;
fill: #abd200;
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: #68b587;
}
@supports (-moz-appearance: auto) {
/* Selector detects Firefox */
.stat {
font-size: 12px;
}
}
.rank-text {
font: 800 24px "Segoe UI", Ubuntu, Sans-Serif;
fill: #68b587;
animation: scaleInAnimation 0.3s ease-in-out forwards;
}
.not_bold {
font-weight: 400;
}
.bold {
font-weight: 700;
}
.icon {
fill: #b7d364;
display: block;
}
.rank-circle-rim {
stroke: #abd200;
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: #abd200;
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;
}
* {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
</style>
<rect
data-testid="card-bg"
x="0.5"
y="0.5"
rx="4.5"
height="99%"
stroke="#0044ff"
width="466"
fill="#0a0f0b"
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">
Cateline Mnemosyne's GitHub Stats
</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<g data-testid="rank-circle" transform="translate(390.5, 47.5)">
<circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
<circle class="rank-circle" cx="-10" cy="8" r="40" />
<g class="rank-text">
<text
x="-5"
y="3"
alignment-baseline="central"
dominant-baseline="central"
text-anchor="middle"
>
A+
</text>
</g>
</g>
<svg x="0" y="0">
<g transform="translate(0, 0)">
<g
class="stagger"
style="animation-delay: 450ms"
transform="translate(25, 0)"
>
<svg
data-testid="icon"
class="icon"
viewBox="0 0 16 16"
version="1.1"
width="16"
height="16"
>
<path
fill-rule="evenodd"
d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"
/>
</svg>
<text class="stat bold" x="25" y="12.5">Total Stars Earned:</text>
<text class="stat bold" x="219.01" y="12.5" data-testid="stars">
1
</text>
</g>
</g>
<g transform="translate(0, 25)">
<g
class="stagger"
style="animation-delay: 600ms"
transform="translate(25, 0)"
>
<svg
data-testid="icon"
class="icon"
viewBox="0 0 16 16"
version="1.1"
width="16"
height="16"
>
<path
fill-rule="evenodd"
d="M1.643 3.143L.427 1.927A.25.25 0 000 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 00.177-.427L2.715 4.215a6.5 6.5 0 11-1.18 4.458.75.75 0 10-1.493.154 8.001 8.001 0 101.6-5.684zM7.75 4a.75.75 0 01.75.75v2.992l2.028.812a.75.75 0 01-.557 1.392l-2.5-1A.75.75 0 017 8.25v-3.5A.75.75 0 017.75 4z"
/>
</svg>
<text class="stat bold" x="25" y="12.5">Total Commits (2023):</text>
<text class="stat bold" x="219.01" y="12.5" data-testid="commits">
7
</text>
</g>
</g>
<g transform="translate(0, 50)">
<g
class="stagger"
style="animation-delay: 750ms"
transform="translate(25, 0)"
>
<svg
data-testid="icon"
class="icon"
viewBox="0 0 16 16"
version="1.1"
width="16"
height="16"
>
<path
fill-rule="evenodd"
d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"
/>
</svg>
<text class="stat bold" x="25" y="12.5">Total PRs:</text>
<text class="stat bold" x="219.01" y="12.5" data-testid="prs">1</text>
</g>
</g>
<g transform="translate(0, 75)">
<g
class="stagger"
style="animation-delay: 900ms"
transform="translate(25, 0)"
>
<svg
data-testid="icon"
class="icon"
viewBox="0 0 16 16"
version="1.1"
width="16"
height="16"
>
<path
fill-rule="evenodd"
d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"
/>
</svg>
<text class="stat bold" x="25" y="12.5">Total Issues:</text>
<text class="stat bold" x="219.01" y="12.5" data-testid="issues">
1
</text>
</g>
</g>
<g transform="translate(0, 100)">
<g
class="stagger"
style="animation-delay: 1050ms"
transform="translate(25, 0)"
>
<svg
data-testid="icon"
class="icon"
viewBox="0 0 16 16"
version="1.1"
width="16"
height="16"
>
<path
fill-rule="evenodd"
d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"
/>
</svg>
<text class="stat bold" x="25" y="12.5">
Contributed to (last year):
</text>
<text class="stat bold" x="219.01" y="12.5" data-testid="contribs">
1
</text>
</g>
</g>
</svg>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7.5 KiB

@@ -1,179 +0,0 @@
<svg
width="450"
height="195"
viewBox="0 0 450 195"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="descId"
>
<title id="titleId">
Hello world, this is a very very very very very long title, Rank: A+
</title>
<desc id="descId">
Total Stars Earned: 1, Total Commits in 2023 : 1, Total PRs: 1, Total
Issues: 1, Contributed to (last year): 1
</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;
}
}
.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: #2f80ed;
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: #2f80ed;
stroke-dasharray: 250;
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
* {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
</style>
<rect
data-testid="card-bg"
x="0.5"
y="0.5"
rx="4.5"
height="99%"
stroke="#e4e2e2"
width="449"
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">
Hello world, this is a very very very very very long title
</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<g data-testid="rank-circle" transform="translate(365, 47.5)">
<circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
<circle class="rank-circle" cx="-10" cy="8" r="40" />
<g class="rank-text">
<text
x="-5"
y="3"
alignment-baseline="central"
dominant-baseline="central"
text-anchor="middle"
>
A+
</text>
</g>
</g>
<svg x="0" y="0">
<g transform="translate(0, 0)">
<g
class="stagger"
style="animation-delay: 450ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Stars Earned:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="stars">
1
</text>
</g>
</g>
<g transform="translate(0, 25)">
<g
class="stagger"
style="animation-delay: 600ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Commits (2023):</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="commits">
7
</text>
</g>
</g>
<g transform="translate(0, 50)">
<g
class="stagger"
style="animation-delay: 750ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total PRs:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="prs">1</text>
</g>
</g>
<g transform="translate(0, 75)">
<g
class="stagger"
style="animation-delay: 900ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Issues:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="issues">
1
</text>
</g>
</g>
<g transform="translate(0, 100)">
<g
class="stagger"
style="animation-delay: 1050ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Contributed to (last year):</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="contribs">
1
</text>
</g>
</g>
</svg>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

@@ -1,164 +0,0 @@
<svg
width="524.375"
height="195"
viewBox="0 0 524.375 195"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="descId"
>
<title id="titleId">
Hello world, this is a very very very very very long title, Rank: A+
</title>
<desc id="descId">
Total Stars Earned: 1, Total Commits in 2023 : 1, Total PRs: 1, Total
Issues: 1, Contributed to (last year): 1
</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;
}
}
.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: #2f80ed;
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: #2f80ed;
stroke-dasharray: 250;
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
* {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
</style>
<rect
data-testid="card-bg"
x="0.5"
y="0.5"
rx="4.5"
height="99%"
stroke="#e4e2e2"
width="523.375"
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">
Hello world, this is a very very very very very long title
</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<svg x="0" y="0">
<g transform="translate(0, 0)">
<g
class="stagger"
style="animation-delay: 450ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Stars Earned:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="stars">
1
</text>
</g>
</g>
<g transform="translate(0, 25)">
<g
class="stagger"
style="animation-delay: 600ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Commits (2023):</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="commits">
7
</text>
</g>
</g>
<g transform="translate(0, 50)">
<g
class="stagger"
style="animation-delay: 750ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total PRs:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="prs">1</text>
</g>
</g>
<g transform="translate(0, 75)">
<g
class="stagger"
style="animation-delay: 900ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Issues:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="issues">
1
</text>
</g>
</g>
<g transform="translate(0, 100)">
<g
class="stagger"
style="animation-delay: 1050ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Contributed to (last year):</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="contribs">
1
</text>
</g>
</g>
</svg>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

@@ -1,164 +0,0 @@
<svg
width="524.375"
height="195"
viewBox="0 0 524.375 195"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="descId"
>
<title id="titleId">
Hello world, this is a very very very very very long title, Rank: A+
</title>
<desc id="descId">
Total Stars Earned: 1, Total Commits in 2023 : 1, Total PRs: 1, Total
Issues: 1, Contributed to (last year): 1
</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;
}
}
.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: #2f80ed;
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: #2f80ed;
stroke-dasharray: 250;
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
* {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
</style>
<rect
data-testid="card-bg"
x="0.5"
y="0.5"
rx="4.5"
height="99%"
stroke="#e4e2e2"
width="523.375"
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">
Hello world, this is a very very very very very long title
</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<svg x="0" y="0">
<g transform="translate(0, 0)">
<g
class="stagger"
style="animation-delay: 450ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Stars Earned:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="stars">
1
</text>
</g>
</g>
<g transform="translate(0, 25)">
<g
class="stagger"
style="animation-delay: 600ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Commits (2023):</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="commits">
7
</text>
</g>
</g>
<g transform="translate(0, 50)">
<g
class="stagger"
style="animation-delay: 750ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total PRs:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="prs">1</text>
</g>
</g>
<g transform="translate(0, 75)">
<g
class="stagger"
style="animation-delay: 900ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Issues:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="issues">
1
</text>
</g>
</g>
<g transform="translate(0, 100)">
<g
class="stagger"
style="animation-delay: 1050ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Contributed to (last year):</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="contribs">
1
</text>
</g>
</g>
</svg>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

@@ -1,152 +0,0 @@
<svg
width="450"
height="150"
viewBox="0 0 450 150"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="descId"
>
<title id="titleId">Cateline Mnemosyne's GitHub Stats, Rank: A+</title>
<desc id="descId">
Total PRs: 1, Total Issues: 1, Contributed to (last year): 1
</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;
}
}
.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: #2f80ed;
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: #2f80ed;
stroke-dasharray: 250;
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
* {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
</style>
<rect
data-testid="card-bg"
x="0.5"
y="0.5"
rx="4.5"
height="99%"
stroke="#e4e2e2"
width="449"
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">
Cateline Mnemosyne's GitHub Stats
</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<g data-testid="rank-circle" transform="translate(365, 25)">
<circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
<circle class="rank-circle" cx="-10" cy="8" r="40" />
<g class="rank-text">
<text
x="-5"
y="3"
alignment-baseline="central"
dominant-baseline="central"
text-anchor="middle"
>
A+
</text>
</g>
</g>
<svg x="0" y="0">
<g transform="translate(0, 0)">
<g
class="stagger"
style="animation-delay: 450ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total PRs:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="prs">1</text>
</g>
</g>
<g transform="translate(0, 25)">
<g
class="stagger"
style="animation-delay: 600ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Issues:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="issues">
1
</text>
</g>
</g>
<g transform="translate(0, 50)">
<g
class="stagger"
style="animation-delay: 750ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Contributed to (last year):</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="contribs">
1
</text>
</g>
</g>
</svg>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

@@ -1,177 +0,0 @@
<svg
width="450"
height="195"
viewBox="0 0 450 195"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="descId"
>
<title id="titleId">Cateline Mnemosyne's GitHub Stats, Rank: A+</title>
<desc id="descId">
Total Stars Earned: 1, Total Commits in 2023 : 1, Total PRs: 1, Total
Issues: 1, Contributed to (last year): 1
</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;
}
}
.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: #2f80ed;
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: #2f80ed;
stroke-dasharray: 250;
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
* {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
</style>
<rect
data-testid="card-bg"
x="0.5"
y="0.5"
rx="4.5"
height="99%"
stroke="#e4e2e2"
width="449"
fill="#fffefe"
stroke-opacity="0"
/>
<g data-testid="card-title" transform="translate(25, 35)">
<g transform="translate(0, 0)">
<text x="0" y="0" class="header" data-testid="header">
Cateline Mnemosyne's GitHub Stats
</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<g data-testid="rank-circle" transform="translate(365, 47.5)">
<circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
<circle class="rank-circle" cx="-10" cy="8" r="40" />
<g class="rank-text">
<text
x="-5"
y="3"
alignment-baseline="central"
dominant-baseline="central"
text-anchor="middle"
>
A+
</text>
</g>
</g>
<svg x="0" y="0">
<g transform="translate(0, 0)">
<g
class="stagger"
style="animation-delay: 450ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Stars Earned:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="stars">
1
</text>
</g>
</g>
<g transform="translate(0, 25)">
<g
class="stagger"
style="animation-delay: 600ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Commits (2023):</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="commits">
7
</text>
</g>
</g>
<g transform="translate(0, 50)">
<g
class="stagger"
style="animation-delay: 750ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total PRs:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="prs">1</text>
</g>
</g>
<g transform="translate(0, 75)">
<g
class="stagger"
style="animation-delay: 900ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Issues:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="issues">
1
</text>
</g>
</g>
<g transform="translate(0, 100)">
<g
class="stagger"
style="animation-delay: 1050ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Contributed to (last year):</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="contribs">
1
</text>
</g>
</g>
</svg>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

@@ -1,162 +0,0 @@
<svg
width="370.1875000000001"
height="195"
viewBox="0 0 370.1875000000001 195"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="descId"
>
<title id="titleId">Cateline Mnemosyne's GitHub Stats, Rank: A+</title>
<desc id="descId">
Total Stars Earned: 1, Total Commits in 2023 : 1, Total PRs: 1, Total
Issues: 1, Contributed to (last year): 1
</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;
}
}
.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: #2f80ed;
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: #2f80ed;
stroke-dasharray: 250;
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
* {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
</style>
<rect
data-testid="card-bg"
x="0.5"
y="0.5"
rx="4.5"
height="99%"
stroke="#e4e2e2"
width="369.1875000000001"
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">
Cateline Mnemosyne's GitHub Stats
</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<svg x="0" y="0">
<g transform="translate(0, 0)">
<g
class="stagger"
style="animation-delay: 450ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Stars Earned:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="stars">
1
</text>
</g>
</g>
<g transform="translate(0, 25)">
<g
class="stagger"
style="animation-delay: 600ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Commits (2023):</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="commits">
7
</text>
</g>
</g>
<g transform="translate(0, 50)">
<g
class="stagger"
style="animation-delay: 750ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total PRs:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="prs">1</text>
</g>
</g>
<g transform="translate(0, 75)">
<g
class="stagger"
style="animation-delay: 900ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Issues:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="issues">
1
</text>
</g>
</g>
<g transform="translate(0, 100)">
<g
class="stagger"
style="animation-delay: 1050ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Contributed to (last year):</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="contribs">
1
</text>
</g>
</g>
</svg>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

@@ -1,169 +0,0 @@
<svg
width="450"
height="165"
viewBox="0 0 450 165"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="descId"
>
<title id="titleId">Cateline Mnemosyne's GitHub Stats, Rank: A+</title>
<desc id="descId">
Total Stars Earned: 1, Total Commits in 2023 : 1, Total PRs: 1, Total
Issues: 1, Contributed to (last year): 1
</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;
}
}
.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: #2f80ed;
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: #2f80ed;
stroke-dasharray: 250;
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
* {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
</style>
<rect
data-testid="card-bg"
x="0.5"
y="0.5"
rx="4.5"
height="99%"
stroke="#e4e2e2"
width="449"
fill="#fffefe"
stroke-opacity="1"
/>
<g data-testid="main-card-body" transform="translate(0, 25)">
<g data-testid="rank-circle" transform="translate(365, 47.5)">
<circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
<circle class="rank-circle" cx="-10" cy="8" r="40" />
<g class="rank-text">
<text
x="-5"
y="3"
alignment-baseline="central"
dominant-baseline="central"
text-anchor="middle"
>
A+
</text>
</g>
</g>
<svg x="0" y="0">
<g transform="translate(0, 0)">
<g
class="stagger"
style="animation-delay: 450ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Stars Earned:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="stars">
1
</text>
</g>
</g>
<g transform="translate(0, 25)">
<g
class="stagger"
style="animation-delay: 600ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Commits (2023):</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="commits">
7
</text>
</g>
</g>
<g transform="translate(0, 50)">
<g
class="stagger"
style="animation-delay: 750ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total PRs:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="prs">1</text>
</g>
</g>
<g transform="translate(0, 75)">
<g
class="stagger"
style="animation-delay: 900ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Issues:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="issues">
1
</text>
</g>
</g>
<g transform="translate(0, 100)">
<g
class="stagger"
style="animation-delay: 1050ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Contributed to (last year):</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="contribs">
1
</text>
</g>
</g>
</svg>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

@@ -1,249 +0,0 @@
<svg
width="467"
height="645"
viewBox="0 0 467 645"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="descId"
>
<title id="titleId">Cateline Mnemosyne's GitHub Stats, Rank: A+</title>
<desc id="descId">
Total Stars Earned: 1, Total Commits in 2023 : 1, Total PRs: 1, Total
Issues: 1, Contributed to (last year): 1
</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;
}
}
.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: block;
}
.rank-circle-rim {
stroke: #2f80ed;
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: #2f80ed;
stroke-dasharray: 250;
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
* {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
</style>
<rect
data-testid="card-bg"
x="0.5"
y="0.5"
rx="4.5"
height="99%"
stroke="#e4e2e2"
width="466"
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">
Cateline Mnemosyne's GitHub Stats
</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<g data-testid="rank-circle" transform="translate(390.5, 272.5)">
<circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
<circle class="rank-circle" cx="-10" cy="8" r="40" />
<g class="rank-text">
<text
x="-5"
y="3"
alignment-baseline="central"
dominant-baseline="central"
text-anchor="middle"
>
A+
</text>
</g>
</g>
<svg x="0" y="0">
<g transform="translate(0, 0)">
<g
class="stagger"
style="animation-delay: 450ms"
transform="translate(25, 0)"
>
<svg
data-testid="icon"
class="icon"
viewBox="0 0 16 16"
version="1.1"
width="16"
height="16"
>
<path
fill-rule="evenodd"
d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"
/>
</svg>
<text class="stat bold" x="25" y="12.5">Total Stars Earned:</text>
<text class="stat bold" x="219.01" y="12.5" data-testid="stars">
1
</text>
</g>
</g>
<g transform="translate(0, 100)">
<g
class="stagger"
style="animation-delay: 600ms"
transform="translate(25, 0)"
>
<svg
data-testid="icon"
class="icon"
viewBox="0 0 16 16"
version="1.1"
width="16"
height="16"
>
<path
fill-rule="evenodd"
d="M1.643 3.143L.427 1.927A.25.25 0 000 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 00.177-.427L2.715 4.215a6.5 6.5 0 11-1.18 4.458.75.75 0 10-1.493.154 8.001 8.001 0 101.6-5.684zM7.75 4a.75.75 0 01.75.75v2.992l2.028.812a.75.75 0 01-.557 1.392l-2.5-1A.75.75 0 017 8.25v-3.5A.75.75 0 017.75 4z"
/>
</svg>
<text class="stat bold" x="25" y="12.5">Total Commits (2023):</text>
<text class="stat bold" x="219.01" y="12.5" data-testid="commits">
7
</text>
</g>
</g>
<g transform="translate(0, 200)">
<g
class="stagger"
style="animation-delay: 750ms"
transform="translate(25, 0)"
>
<svg
data-testid="icon"
class="icon"
viewBox="0 0 16 16"
version="1.1"
width="16"
height="16"
>
<path
fill-rule="evenodd"
d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"
/>
</svg>
<text class="stat bold" x="25" y="12.5">Total PRs:</text>
<text class="stat bold" x="219.01" y="12.5" data-testid="prs">1</text>
</g>
</g>
<g transform="translate(0, 300)">
<g
class="stagger"
style="animation-delay: 900ms"
transform="translate(25, 0)"
>
<svg
data-testid="icon"
class="icon"
viewBox="0 0 16 16"
version="1.1"
width="16"
height="16"
>
<path
fill-rule="evenodd"
d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"
/>
</svg>
<text class="stat bold" x="25" y="12.5">Total Issues:</text>
<text class="stat bold" x="219.01" y="12.5" data-testid="issues">
1
</text>
</g>
</g>
<g transform="translate(0, 400)">
<g
class="stagger"
style="animation-delay: 1050ms"
transform="translate(25, 0)"
>
<svg
data-testid="icon"
class="icon"
viewBox="0 0 16 16"
version="1.1"
width="16"
height="16"
>
<path
fill-rule="evenodd"
d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"
/>
</svg>
<text class="stat bold" x="25" y="12.5">
Contributed to (last year):
</text>
<text class="stat bold" x="219.01" y="12.5" data-testid="contribs">
1
</text>
</g>
</g>
</svg>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7.5 KiB

@@ -1,185 +0,0 @@
<svg
width="450"
height="195"
viewBox="0 0 450 195"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="descId"
>
<title id="titleId">
&#1057;&#1090;&#1072;&#1090;&#1080;&#1089;&#1090;&#1080;&#1082;&#1072;
GitHub
&#1087;&#1086;&#1083;&#1100;&#1079;&#1086;&#1074;&#1072;&#1090;&#1077;&#1083;&#1103;
Cateline Mnemosyne, Rank: A+
</title>
<desc id="descId">
Всего звезд: 1, Всего коммитов in 2023 : 1, Всего pull request`ов: 1, Всего
issue: 1, Внёс вклад в (last year): 1
</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;
}
}
.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: #2f80ed;
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: #2f80ed;
stroke-dasharray: 250;
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
* {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
</style>
<rect
data-testid="card-bg"
x="0.5"
y="0.5"
rx="4.5"
height="99%"
stroke="#e4e2e2"
width="449"
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">
&#1057;&#1090;&#1072;&#1090;&#1080;&#1089;&#1090;&#1080;&#1082;&#1072;
GitHub
&#1087;&#1086;&#1083;&#1100;&#1079;&#1086;&#1074;&#1072;&#1090;&#1077;&#1083;&#1103;
Cateline Mnemosyne
</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<g data-testid="rank-circle" transform="translate(365, 47.5)">
<circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
<circle class="rank-circle" cx="-10" cy="8" r="40" />
<g class="rank-text">
<text
x="-5"
y="3"
alignment-baseline="central"
dominant-baseline="central"
text-anchor="middle"
>
A+
</text>
</g>
</g>
<svg x="0" y="0">
<g transform="translate(0, 0)">
<g
class="stagger"
style="animation-delay: 450ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Всего звезд:</text>
<text class="stat bold" x="249.01" y="12.5" data-testid="stars">
1
</text>
</g>
</g>
<g transform="translate(0, 25)">
<g
class="stagger"
style="animation-delay: 600ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Всего коммитов (2023):</text>
<text class="stat bold" x="249.01" y="12.5" data-testid="commits">
7
</text>
</g>
</g>
<g transform="translate(0, 50)">
<g
class="stagger"
style="animation-delay: 750ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Всего pull request`ов:</text>
<text class="stat bold" x="249.01" y="12.5" data-testid="prs">1</text>
</g>
</g>
<g transform="translate(0, 75)">
<g
class="stagger"
style="animation-delay: 900ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Всего issue:</text>
<text class="stat bold" x="249.01" y="12.5" data-testid="issues">
1
</text>
</g>
</g>
<g transform="translate(0, 100)">
<g
class="stagger"
style="animation-delay: 1050ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Внёс вклад в (last year):</text>
<text class="stat bold" x="249.01" y="12.5" data-testid="contribs">
1
</text>
</g>
</g>
</svg>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

@@ -1,177 +0,0 @@
<svg
width="450"
height="195"
viewBox="0 0 450 195"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="descId"
>
<title id="titleId">Cateline Mnemosyne's GitHub Stats, Rank: A+</title>
<desc id="descId">
Total Stars Earned: 1, Total Commits in 2023 : 1, Total PRs: 1, Total
Issues: 1, Contributed to (last year): 1
</desc>
<style>
.header {
font: 600 18px "Segoe UI", Ubuntu, Sans-Serif;
fill: #fe428e;
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: #a9fef7;
}
@supports (-moz-appearance: auto) {
/* Selector detects Firefox */
.stat {
font-size: 12px;
}
}
.rank-text {
font: 800 24px "Segoe UI", Ubuntu, Sans-Serif;
fill: #a9fef7;
animation: scaleInAnimation 0.3s ease-in-out forwards;
}
.not_bold {
font-weight: 400;
}
.bold {
font-weight: 700;
}
.icon {
fill: #f8d847;
display: none;
}
.rank-circle-rim {
stroke: #fe428e;
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: #fe428e;
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;
}
* {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
</style>
<rect
data-testid="card-bg"
x="0.5"
y="0.5"
rx="4.5"
height="99%"
stroke="#e4e2e2"
width="449"
fill="#141321"
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">
Cateline Mnemosyne's GitHub Stats
</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<g data-testid="rank-circle" transform="translate(365, 47.5)">
<circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
<circle class="rank-circle" cx="-10" cy="8" r="40" />
<g class="rank-text">
<text
x="-5"
y="3"
alignment-baseline="central"
dominant-baseline="central"
text-anchor="middle"
>
A+
</text>
</g>
</g>
<svg x="0" y="0">
<g transform="translate(0, 0)">
<g
class="stagger"
style="animation-delay: 450ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Stars Earned:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="stars">
1
</text>
</g>
</g>
<g transform="translate(0, 25)">
<g
class="stagger"
style="animation-delay: 600ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Commits (2023):</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="commits">
7
</text>
</g>
</g>
<g transform="translate(0, 50)">
<g
class="stagger"
style="animation-delay: 750ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total PRs:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="prs">1</text>
</g>
</g>
<g transform="translate(0, 75)">
<g
class="stagger"
style="animation-delay: 900ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Issues:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="issues">
1
</text>
</g>
</g>
<g transform="translate(0, 100)">
<g
class="stagger"
style="animation-delay: 1050ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Contributed to (last year):</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="contribs">
1
</text>
</g>
</g>
</svg>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

@@ -1,177 +0,0 @@
<svg
width="450"
height="195"
viewBox="0 0 450 195"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="descId"
>
<title id="titleId">Cateline Mnemosyne's GitHub Stats, Rank: A+</title>
<desc id="descId">
Total Stars Earned: 1, Total Commits in 2023 : 1, Total PRs: 1, Total
Issues: 1, Contributed to (last year): 1
</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;
}
}
.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: #2f80ed;
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: #2f80ed;
stroke-dasharray: 250;
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
* {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
</style>
<rect
data-testid="card-bg"
x="0.5"
y="0.5"
rx="4.5"
height="99%"
stroke="#e4e2e2"
width="449"
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">
Cateline Mnemosyne's GitHub Stats
</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<g data-testid="rank-circle" transform="translate(365, 47.5)">
<circle class="rank-circle-rim" cx="-10" cy="8" r="40" />
<circle class="rank-circle" cx="-10" cy="8" r="40" />
<g class="rank-text">
<text
x="-5"
y="3"
alignment-baseline="central"
dominant-baseline="central"
text-anchor="middle"
>
A+
</text>
</g>
</g>
<svg x="0" y="0">
<g transform="translate(0, 0)">
<g
class="stagger"
style="animation-delay: 450ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Stars Earned:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="stars">
1
</text>
</g>
</g>
<g transform="translate(0, 25)">
<g
class="stagger"
style="animation-delay: 600ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Commits (2023):</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="commits">
7
</text>
</g>
</g>
<g transform="translate(0, 50)">
<g
class="stagger"
style="animation-delay: 750ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total PRs:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="prs">1</text>
</g>
</g>
<g transform="translate(0, 75)">
<g
class="stagger"
style="animation-delay: 900ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Total Issues:</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="issues">
1
</text>
</g>
</g>
<g transform="translate(0, 100)">
<g
class="stagger"
style="animation-delay: 1050ms"
transform="translate(25, 0)"
>
<text class="stat bold" y="12.5">Contributed to (last year):</text>
<text class="stat bold" x="199.01" y="12.5" data-testid="contribs">
1
</text>
</g>
</g>
</svg>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

-152
View File
@@ -1,152 +0,0 @@
import "jest-svg-snapshot";
import "@testing-library/jest-dom";
import renderStatsCard from "../../src/cards/stats-card";
import prettier from "prettier";
const STATS_DATA = {
name: "Cateline Mnemosyne",
totalPRs: 1,
totalCommits: 7,
totalIssues: 1,
totalStars: 1,
contributedTo: 1,
rank: {
level: "A+",
score: 50.893750297869225,
},
};
const format = (svg) => prettier.format(svg, { parser: "html" });
describe("statsCard", () => {
it("should match default stat card", () => {
const svg = format(
renderStatsCard(STATS_DATA, { disable_animations: true }),
);
expect(svg).toMatchSVGSnapshot();
});
it("option hide", () => {
const svg = format(
renderStatsCard(STATS_DATA, {
disable_animations: true,
hide: ["commits", "stars"],
}),
);
expect(svg).toMatchSVGSnapshot();
});
it("option hide_border", () => {
const svg = format(
renderStatsCard(STATS_DATA, {
disable_animations: true,
hide_border: true,
}),
);
expect(svg).toMatchSVGSnapshot();
});
it("option hide_title", () => {
const svg = format(
renderStatsCard(STATS_DATA, {
disable_animations: true,
hide_title: true,
}),
);
expect(svg).toMatchSVGSnapshot();
});
it("option hide_rank", () => {
const svg = format(
renderStatsCard(STATS_DATA, {
disable_animations: true,
hide_rank: true,
}),
);
expect(svg).toMatchSVGSnapshot();
});
it("option theme", () => {
const svg = format(
renderStatsCard(STATS_DATA, {
disable_animations: true,
theme: "radical",
}),
);
expect(svg).toMatchSVGSnapshot();
});
it("option card_width", () => {
const svg = format(
renderStatsCard(STATS_DATA, {
disable_animations: true,
card_width: 800,
}),
);
expect(svg).toMatchSVGSnapshot();
});
it("option custom_title", () => {
const svg = format(
renderStatsCard(STATS_DATA, {
disable_animations: true,
custom_title:
"Hello world, this is a very very very very very long title",
}),
);
expect(svg).toMatchSVGSnapshot();
});
it("option custom_title hideRank", () => {
const svg = format(
renderStatsCard(STATS_DATA, {
disable_animations: true,
hide_rank: true,
custom_title:
"Hello world, this is a very very very very very long title",
}),
);
expect(svg).toMatchSVGSnapshot();
});
it("option custom_title hideRank card_width", () => {
const svg = format(
renderStatsCard(STATS_DATA, {
disable_animations: true,
card_width: 300,
hide_rank: true,
custom_title:
"Hello world, this is a very very very very very long title",
}),
);
expect(svg).toMatchSVGSnapshot();
});
it("option line_height show_icons", () => {
const svg = format(
renderStatsCard(STATS_DATA, {
disable_animations: true,
line_height: 100,
show_icons: true,
}),
);
expect(svg).toMatchSVGSnapshot();
});
it("option locale", () => {
const svg = format(
renderStatsCard(STATS_DATA, {
disable_animations: true,
locale: "ru",
}),
);
expect(svg).toMatchSVGSnapshot();
});
it("option colors", () => {
const svg = format(
renderStatsCard(STATS_DATA, {
disable_animations: true,
border_color: "0044ff",
title_color: "abd200",
icon_color: "b7d364",
text_color: "68b587",
bg_color: "0a0f0b",
show_icons: true,
}),
);
expect(svg).toMatchSVGSnapshot();
});
});
+1 -1
View File
@@ -2,7 +2,7 @@
"functions": {
"api/*.js": {
"memory": 128,
"maxDuration": 10
"maxDuration": 30
}
},
"redirects": [