mirror of
https://github.com/anuraghazra/github-readme-stats.git
synced 2025-02-17 14:40:18 +08:00
refactor(cards): added typings for cards and fetchers (#1596)
* refactor(cards): added typings for cards and fetchers * chore: move types to separate file
This commit is contained in:
parent
8b9f9317d8
commit
d57251cdf1
@ -47,10 +47,10 @@ module.exports = async (req, res) => {
|
||||
);
|
||||
|
||||
/*
|
||||
if star count & fork count is over 1k then we are kFormating the text
|
||||
and if both are zero we are not showing the stats
|
||||
so we can just make the cache longer, since there is no need to frequent updates
|
||||
*/
|
||||
if star count & fork count is over 1k then we are kFormating the text
|
||||
and if both are zero we are not showing the stats
|
||||
so we can just make the cache longer, since there is no need to frequent updates
|
||||
*/
|
||||
const stars = repoData.starCount;
|
||||
const forks = repoData.forkCount;
|
||||
const isBothOver1K = stars > 1000 && forks > 1000;
|
||||
|
@ -1,3 +1,4 @@
|
||||
// @ts-check
|
||||
const {
|
||||
kFormatter,
|
||||
encodeHTML,
|
||||
@ -65,6 +66,11 @@ const iconWithLabel = (icon, label, testid) => {
|
||||
return flexLayout({ items: [iconSvg, text], gap: 20 }).join("");
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import('../fetchers/types').RepositoryData} repo
|
||||
* @param {Partial<import("./types").RepoCardOptions>} options
|
||||
* @returns {string}
|
||||
*/
|
||||
const renderRepoCard = (repo, options = {}) => {
|
||||
const {
|
||||
name,
|
||||
@ -161,8 +167,10 @@ const renderRepoCard = (repo, options = {}) => {
|
||||
return card.render(`
|
||||
${
|
||||
isTemplate
|
||||
// @ts-ignore
|
||||
? getBadgeSVG(i18n.t("repocard.template"), colors.textColor)
|
||||
: isArchived
|
||||
// @ts-ignore
|
||||
? getBadgeSVG(i18n.t("repocard.archived"), colors.textColor)
|
||||
: ""
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
// @ts-check
|
||||
const I18n = require("../common/I18n");
|
||||
const Card = require("../common/Card");
|
||||
const icons = require("../common/icons");
|
||||
@ -45,6 +46,12 @@ const createTextNode = ({
|
||||
`;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {Partial<import('../fetchers/types').StatsData>} stats
|
||||
* @param {Partial<import("./types").StatCardOptions>} options
|
||||
* @returns {string}
|
||||
*/
|
||||
const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
const {
|
||||
name,
|
||||
@ -75,7 +82,7 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
disable_animations = false,
|
||||
} = options;
|
||||
|
||||
const lheight = parseInt(line_height, 10);
|
||||
const lheight = parseInt(String(line_height), 10);
|
||||
|
||||
// returns theme based colors with proper overrides and defaults
|
||||
const { titleColor, textColor, iconColor, bgColor, borderColor } =
|
||||
|
@ -1,3 +1,4 @@
|
||||
// @ts-check
|
||||
const Card = require("../common/Card");
|
||||
const I18n = require("../common/I18n");
|
||||
const { langCardLocales } = require("../translations");
|
||||
@ -16,6 +17,28 @@ const DEFAULT_LANGS_COUNT = 5;
|
||||
const DEFAULT_LANG_COLOR = "#858585";
|
||||
const CARD_PADDING = 25;
|
||||
|
||||
/**
|
||||
* @typedef {import("../fetchers/types").Lang} Lang
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Lang[]} arr
|
||||
*/
|
||||
const getLongestLang = (arr) =>
|
||||
arr.reduce(
|
||||
(savedLang, lang) =>
|
||||
lang.name.length > savedLang.name.length ? lang : savedLang,
|
||||
{ name: "", size: null, color: "" },
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* width: number,
|
||||
* color: string,
|
||||
* name: string,
|
||||
* progress: string
|
||||
* }} props
|
||||
*/
|
||||
const createProgressTextNode = ({ width, color, name, progress }) => {
|
||||
const paddingRight = 95;
|
||||
const progressTextX = width - paddingRight + 10;
|
||||
@ -35,6 +58,9 @@ const createProgressTextNode = ({ width, color, name, progress }) => {
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {{ lang: Lang, totalSize: number }} props
|
||||
*/
|
||||
const createCompactLangNode = ({ lang, totalSize }) => {
|
||||
const percentage = ((lang.size / totalSize) * 100).toFixed(2);
|
||||
const color = lang.color || "#858585";
|
||||
@ -49,21 +75,19 @@ const createCompactLangNode = ({ lang, totalSize }) => {
|
||||
`;
|
||||
};
|
||||
|
||||
const getLongestLang = (arr) =>
|
||||
arr.reduce(
|
||||
(savedLang, lang) =>
|
||||
lang.name.length > savedLang.name.length ? lang : savedLang,
|
||||
{ name: "" },
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {{ langs: Lang[], totalSize: number }} props
|
||||
*/
|
||||
const createLanguageTextNode = ({ langs, totalSize }) => {
|
||||
const longestLang = getLongestLang(langs);
|
||||
const chunked = chunkArray(langs, langs.length / 2);
|
||||
const layouts = chunked.map((array) => {
|
||||
// @ts-ignore
|
||||
const items = array.map((lang, index) =>
|
||||
createCompactLangNode({
|
||||
lang,
|
||||
totalSize,
|
||||
// @ts-ignore
|
||||
index,
|
||||
}),
|
||||
);
|
||||
@ -84,8 +108,7 @@ const createLanguageTextNode = ({ langs, totalSize }) => {
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any[]} langs
|
||||
* @param {Lang[]} langs
|
||||
* @param {number} width
|
||||
* @param {number} totalLanguageSize
|
||||
* @returns {string}
|
||||
@ -106,8 +129,7 @@ const renderNormalLayout = (langs, width, totalLanguageSize) => {
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any[]} langs
|
||||
* @param {Lang[]} langs
|
||||
* @param {number} width
|
||||
* @param {number} totalLanguageSize
|
||||
* @returns {string}
|
||||
@ -152,7 +174,6 @@ const renderCompactLayout = (langs, width, totalLanguageSize) => {
|
||||
${createLanguageTextNode({
|
||||
langs,
|
||||
totalSize: totalLanguageSize,
|
||||
width,
|
||||
})}
|
||||
</g>
|
||||
`;
|
||||
@ -174,6 +195,12 @@ const calculateNormalLayoutHeight = (totalLangs) => {
|
||||
return 45 + (totalLangs + 1) * 40;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Record<string, Lang>} topLangs
|
||||
* @param {string[]} hide
|
||||
* @param {string} langs_count
|
||||
*/
|
||||
const useLanguages = (topLangs, hide, langs_count) => {
|
||||
let langs = Object.values(topLangs);
|
||||
let langsToHide = {};
|
||||
@ -200,6 +227,11 @@ const useLanguages = (topLangs, hide, langs_count) => {
|
||||
return { langs, totalLanguageSize };
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import('../fetchers/types').TopLangData} topLangs
|
||||
* @param {Partial<import("./types").TopLangOptions>} options
|
||||
* @returns {string}
|
||||
*/
|
||||
const renderTopLanguages = (topLangs, options = {}) => {
|
||||
const {
|
||||
hide_title,
|
||||
@ -226,7 +258,7 @@ const renderTopLanguages = (topLangs, options = {}) => {
|
||||
const { langs, totalLanguageSize } = useLanguages(
|
||||
topLangs,
|
||||
hide,
|
||||
langs_count,
|
||||
String(langs_count),
|
||||
);
|
||||
|
||||
let width = isNaN(card_width) ? DEFAULT_CARD_WIDTH : card_width;
|
||||
|
50
src/cards/types.d.ts
vendored
Normal file
50
src/cards/types.d.ts
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
type ThemeNames = keyof typeof import("../../themes");
|
||||
|
||||
export type CommonOptions = {
|
||||
title_color: string;
|
||||
icon_color: string;
|
||||
text_color: string;
|
||||
bg_color: string;
|
||||
theme: ThemeNames;
|
||||
border_radius: number;
|
||||
border_color: string;
|
||||
locale: string;
|
||||
};
|
||||
|
||||
export type StatCardOptions = CommonOptions & {
|
||||
hide: string[];
|
||||
show_icons: boolean;
|
||||
hide_title: boolean;
|
||||
hide_border: boolean;
|
||||
hide_rank: boolean;
|
||||
include_all_commits: boolean;
|
||||
line_height: number | string;
|
||||
custom_title: string;
|
||||
disable_animations: boolean;
|
||||
};
|
||||
|
||||
export type RepoCardOptions = CommonOptions & {
|
||||
hide_border: boolean;
|
||||
show_owner: boolean;
|
||||
};
|
||||
|
||||
export type TopLangOptions = CommonOptions & {
|
||||
hide_title: boolean;
|
||||
hide_border: boolean;
|
||||
card_width: number;
|
||||
hide: string[];
|
||||
layout: "compact" | "normal";
|
||||
custom_title: string;
|
||||
langs_count: number;
|
||||
};
|
||||
|
||||
type WakaTimeOptions = CommonOptions & {
|
||||
hide_title: boolean;
|
||||
hide_border: boolean;
|
||||
hide: string[];
|
||||
line_height: string;
|
||||
hide_progress: boolean;
|
||||
custom_title: string;
|
||||
layout: "compact" | "normal";
|
||||
langs_count: number;
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
// @ts-check
|
||||
const Card = require("../common/Card");
|
||||
const I18n = require("../common/I18n");
|
||||
const { getStyles } = require("../getStyles");
|
||||
@ -11,12 +12,24 @@ const {
|
||||
lowercaseTrim,
|
||||
} = require("../common/utils");
|
||||
|
||||
/**
|
||||
* @param {{color: string, text: string}} param0
|
||||
*/
|
||||
const noCodingActivityNode = ({ color, text }) => {
|
||||
return `
|
||||
<text x="25" y="11" class="stat bold" fill="${color}">${text}</text>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{
|
||||
* lang: import("../fetchers/types").WakaTimeLang,
|
||||
* totalSize: number,
|
||||
* x: number,
|
||||
* y: number
|
||||
* }} props
|
||||
*/
|
||||
const createCompactLangNode = ({ lang, totalSize, x, y }) => {
|
||||
const color = languageColors[lang.name] || "#858585";
|
||||
|
||||
@ -30,6 +43,14 @@ const createCompactLangNode = ({ lang, totalSize, x, y }) => {
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* langs: import("../fetchers/types").WakaTimeLang[],
|
||||
* totalSize: number,
|
||||
* x: number,
|
||||
* y: number
|
||||
* }} props
|
||||
*/
|
||||
const createLanguageTextNode = ({ langs, totalSize, x, y }) => {
|
||||
return langs.map((lang, index) => {
|
||||
if (index % 2 === 0) {
|
||||
@ -38,7 +59,6 @@ const createLanguageTextNode = ({ langs, totalSize, x, y }) => {
|
||||
x: 25,
|
||||
y: 12.5 * index + y,
|
||||
totalSize,
|
||||
index,
|
||||
});
|
||||
}
|
||||
return createCompactLangNode({
|
||||
@ -46,11 +66,23 @@ const createLanguageTextNode = ({ langs, totalSize, x, y }) => {
|
||||
x: 230,
|
||||
y: 12.5 + 12.5 * index,
|
||||
totalSize,
|
||||
index,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{
|
||||
* id: string;
|
||||
* label: string;
|
||||
* value: string;
|
||||
* index: number;
|
||||
* percent: number;
|
||||
* hideProgress: boolean;
|
||||
* progressBarColor: string;
|
||||
* progressBarBackgroundColor: string
|
||||
* }} props
|
||||
*/
|
||||
const createTextNode = ({
|
||||
id,
|
||||
label,
|
||||
@ -71,6 +103,7 @@ const createTextNode = ({
|
||||
progress: percent,
|
||||
color: progressBarColor,
|
||||
width: 220,
|
||||
// @ts-ignore
|
||||
name: label,
|
||||
progressBarBackgroundColor,
|
||||
});
|
||||
@ -88,6 +121,9 @@ const createTextNode = ({
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import("../fetchers/types").WakaTimeLang[]} languages
|
||||
*/
|
||||
const recalculatePercentages = (languages) => {
|
||||
// recalculating percentages so that,
|
||||
// compact layout's progress bar does not break when hiding languages
|
||||
@ -95,12 +131,17 @@ const recalculatePercentages = (languages) => {
|
||||
(totalSum, language) => totalSum + language.percent,
|
||||
0,
|
||||
);
|
||||
const weight = (100 / totalSum).toFixed(2);
|
||||
const weight = +(100 / totalSum).toFixed(2);
|
||||
languages.forEach((language) => {
|
||||
language.percent = (language.percent * weight).toFixed(2);
|
||||
language.percent = +(language.percent * weight).toFixed(2);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Partial<import('../fetchers/types').WakaTimeData>} stats
|
||||
* @param {Partial<import('./types').WakaTimeOptions>} options
|
||||
* @returns {string}
|
||||
*/
|
||||
const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
|
||||
let { languages } = stats;
|
||||
const {
|
||||
@ -136,25 +177,20 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
|
||||
translations: wakatimeCardLocales,
|
||||
});
|
||||
|
||||
const lheight = parseInt(line_height, 10);
|
||||
const lheight = parseInt(String(line_height), 10);
|
||||
|
||||
const langsCount = clampValue(parseInt(langs_count), 1, langs_count);
|
||||
const langsCount = clampValue(parseInt(String(langs_count)), 1, langs_count);
|
||||
|
||||
// returns theme based colors with proper overrides and defaults
|
||||
const {
|
||||
titleColor,
|
||||
textColor,
|
||||
iconColor,
|
||||
bgColor,
|
||||
borderColor,
|
||||
} = getCardColors({
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
});
|
||||
const { titleColor, textColor, iconColor, bgColor, borderColor } =
|
||||
getCardColors({
|
||||
title_color,
|
||||
icon_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
});
|
||||
|
||||
const filteredLanguages = languages
|
||||
? languages
|
||||
@ -228,13 +264,16 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
|
||||
label: language.name,
|
||||
value: language.text,
|
||||
percent: language.percent,
|
||||
// @ts-ignore
|
||||
progressBarColor: titleColor,
|
||||
// @ts-ignore
|
||||
progressBarBackgroundColor: textColor,
|
||||
hideProgress: hide_progress,
|
||||
});
|
||||
})
|
||||
: [
|
||||
noCodingActivityNode({
|
||||
// @ts-ignore
|
||||
color: textColor,
|
||||
text: i18n.t("wakatimecard.nocodingactivity"),
|
||||
}),
|
||||
|
@ -161,11 +161,11 @@ function flexLayout({ items, gap, direction, sizes = [] }) {
|
||||
|
||||
/**
|
||||
* @typedef {object} CardColors
|
||||
* @prop {string} title_color
|
||||
* @prop {string} text_color
|
||||
* @prop {string} icon_color
|
||||
* @prop {string} bg_color
|
||||
* @prop {string} border_color
|
||||
* @prop {string?=} title_color
|
||||
* @prop {string?=} text_color
|
||||
* @prop {string?=} icon_color
|
||||
* @prop {string?=} bg_color
|
||||
* @prop {string?=} border_color
|
||||
* @prop {keyof typeof import('../../themes')?=} fallbackTheme
|
||||
* @prop {keyof typeof import('../../themes')?=} theme
|
||||
*/
|
||||
|
@ -1,6 +1,11 @@
|
||||
// @ts-check
|
||||
const retryer = require("../common/retryer");
|
||||
const { request } = require("../common/utils");
|
||||
|
||||
/**
|
||||
* @param {import('Axios').AxiosRequestHeaders} variables
|
||||
* @param {string} token
|
||||
*/
|
||||
const fetcher = (variables, token) => {
|
||||
return request(
|
||||
{
|
||||
@ -43,6 +48,11 @@ const fetcher = (variables, token) => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} username
|
||||
* @param {string} reponame
|
||||
* @returns {Promise<import("./types").RepositoryData>}
|
||||
*/
|
||||
async function fetchRepo(username, reponame) {
|
||||
if (!username || !reponame) {
|
||||
throw new Error("Invalid username or reponame");
|
||||
|
@ -1,4 +1,5 @@
|
||||
const axios = require("axios");
|
||||
// @ts-check
|
||||
const axios = require("axios").default;
|
||||
const githubUsernameRegex = require("github-username-regex");
|
||||
|
||||
const retryer = require("../common/retryer");
|
||||
@ -7,6 +8,10 @@ const { request, logger, CustomError } = require("../common/utils");
|
||||
|
||||
require("dotenv").config();
|
||||
|
||||
/**
|
||||
* @param {import('axios').AxiosRequestHeaders} variables
|
||||
* @param {string} token
|
||||
*/
|
||||
const fetcher = (variables, token) => {
|
||||
return request(
|
||||
{
|
||||
@ -87,6 +92,12 @@ const totalCommitsFetcher = async (username) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} username
|
||||
* @param {boolean} count_private
|
||||
* @param {boolean} include_all_commits
|
||||
* @returns {Promise<import("./types").StatsData>}
|
||||
*/
|
||||
async function fetchStats(
|
||||
username,
|
||||
count_private = false,
|
||||
|
@ -1,7 +1,12 @@
|
||||
// @ts-check
|
||||
const { request, logger } = require("../common/utils");
|
||||
const retryer = require("../common/retryer");
|
||||
require("dotenv").config();
|
||||
|
||||
/**
|
||||
* @param {import('Axios').AxiosRequestHeaders} variables
|
||||
* @param {string} token
|
||||
*/
|
||||
const fetcher = (variables, token) => {
|
||||
return request(
|
||||
{
|
||||
@ -34,6 +39,11 @@ const fetcher = (variables, token) => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} username
|
||||
* @param {string[]} exclude_repo
|
||||
* @returns {Promise<import("./types").TopLangData>}
|
||||
*/
|
||||
async function fetchTopLanguages(username, exclude_repo = []) {
|
||||
if (!username) throw Error("Invalid username");
|
||||
|
||||
|
104
src/fetchers/types.d.ts
vendored
Normal file
104
src/fetchers/types.d.ts
vendored
Normal file
@ -0,0 +1,104 @@
|
||||
export type RepositoryData = {
|
||||
name: string;
|
||||
nameWithOwner: string;
|
||||
isPrivate: boolean;
|
||||
isArchived: boolean;
|
||||
isTemplate: boolean;
|
||||
stargazers: { totalCount: number };
|
||||
description: string;
|
||||
primaryLanguage: {
|
||||
color: string;
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
forkCount: number;
|
||||
starCount: number;
|
||||
};
|
||||
|
||||
export type StatsData = {
|
||||
name: string;
|
||||
totalPRs: number;
|
||||
totalCommits: number;
|
||||
totalIssues: number;
|
||||
totalStars: number;
|
||||
contributedTo: number;
|
||||
rank: { level: string; score: number };
|
||||
};
|
||||
|
||||
export type Lang = {
|
||||
name: string;
|
||||
color: string;
|
||||
size: number;
|
||||
};
|
||||
|
||||
export type TopLangData = Record<string, Lang>;
|
||||
|
||||
export type WakaTimeData = {
|
||||
categories: {
|
||||
digital: string;
|
||||
hours: number;
|
||||
minutes: number;
|
||||
name: string;
|
||||
percent: number;
|
||||
text: string;
|
||||
total_seconds: number;
|
||||
}[];
|
||||
daily_average: number;
|
||||
daily_average_including_other_language: number;
|
||||
days_including_holidays: number;
|
||||
days_minus_holidays: number;
|
||||
editors: {
|
||||
digital: string;
|
||||
hours: number;
|
||||
minutes: number;
|
||||
name: string;
|
||||
percent: number;
|
||||
text: string;
|
||||
total_seconds: number;
|
||||
}[];
|
||||
holidays: number;
|
||||
human_readable_daily_average: string;
|
||||
human_readable_daily_average_including_other_language: string;
|
||||
human_readable_total: string;
|
||||
human_readable_total_including_other_language: string;
|
||||
id: string;
|
||||
is_already_updating: boolean;
|
||||
is_coding_activity_visible: boolean;
|
||||
is_including_today: boolean;
|
||||
is_other_usage_visible: boolean;
|
||||
is_stuck: boolean;
|
||||
is_up_to_date: boolean;
|
||||
languages: {
|
||||
digital: string;
|
||||
hours: number;
|
||||
minutes: number;
|
||||
name: string;
|
||||
percent: number;
|
||||
text: string;
|
||||
total_seconds: number;
|
||||
}[];
|
||||
operating_systems: {
|
||||
digital: string;
|
||||
hours: number;
|
||||
minutes: number;
|
||||
name: string;
|
||||
percent: number;
|
||||
text: string;
|
||||
total_seconds: number;
|
||||
}[];
|
||||
percent_calculated: number;
|
||||
range: string;
|
||||
status: string;
|
||||
timeout: number;
|
||||
total_seconds: number;
|
||||
total_seconds_including_other_language: number;
|
||||
user_id: string;
|
||||
username: string;
|
||||
writes_only: boolean;
|
||||
};
|
||||
|
||||
export type WakaTimeLang = {
|
||||
name: string;
|
||||
text: string;
|
||||
percent: number;
|
||||
};
|
@ -1,5 +1,9 @@
|
||||
const axios = require("axios");
|
||||
|
||||
/**
|
||||
* @param {{username: string, api_domain: string, range: string}} props
|
||||
* @returns {Promise<WakaTimeData>}
|
||||
*/
|
||||
const fetchWakatimeStats = async ({ username, api_domain, range }) => {
|
||||
try {
|
||||
const { data } = await axios.get(
|
||||
|
@ -1,3 +1,4 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* @param {number} value
|
||||
*/
|
||||
@ -53,11 +54,11 @@ const getAnimations = () => {
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* titleColor: string;
|
||||
* textColor: string;
|
||||
* iconColor: string;
|
||||
* show_icons: boolean;
|
||||
* progress: number;
|
||||
* titleColor?: string | string[]
|
||||
* textColor?: string | string[]
|
||||
* iconColor?: string | string[]
|
||||
* show_icons?: boolean;
|
||||
* progress?: number;
|
||||
* }} args
|
||||
*/
|
||||
const getStyles = ({
|
||||
|
Loading…
Reference in New Issue
Block a user