Skip to content

Commit cb4b5ab

Browse files
authored
Merge pull request #15 from RedHeadphone/issue-6
Issue 6
2 parents 48ccf50 + fe74d55 commit cb4b5ab

26 files changed

+4621
-2998
lines changed

jest.config.js

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
export default {
2-
clearMocks: true,
3-
transform: {},
4-
testEnvironment: "jsdom",
5-
coverageProvider: "v8",
6-
testPathIgnorePatterns: ["<rootDir>/node_modules/"],
7-
modulePathIgnorePatterns: ["<rootDir>/node_modules/"],
8-
coveragePathIgnorePatterns: [
9-
"<rootDir>/node_modules/"
10-
],
11-
};
2+
testEnvironment: "jsdom",
3+
transform: {},
4+
testPathIgnorePatterns: ["<rootDir>/node_modules/"],
5+
modulePathIgnorePatterns: ["<rootDir>/node_modules/"],
6+
coveragePathIgnorePatterns: ["<rootDir>/node_modules/"],
7+
moduleNameMapper: {
8+
"^@/(.*)$": "<rootDir>/src/$1",
9+
},
10+
};

package-lock.json

Lines changed: 1559 additions & 1894 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
"dependencies": {
1313
"@next/font": "13.1.6",
1414
"antd": "^5.3.1",
15-
"axios": "^0.21.4",
16-
"axios-cache-adapter": "^2.7.3",
15+
"axios": "^1.0.0",
16+
"axios-cache-interceptor": "^1.5.3",
1717
"eslint": "8.33.0",
1818
"eslint-config-next": "13.1.6",
1919
"fast-querystring": "^1.1.1",
@@ -25,7 +25,7 @@
2525
"devDependencies": {
2626
"@types/node": "18.11.18",
2727
"@types/react": "18.0.27",
28-
"jest": "^29.4.3",
28+
"jest": "^29.7.0",
2929
"jest-environment-jsdom": "^29.4.3"
3030
}
3131
}

src/cacheAxios.js

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/common.js

Lines changed: 56 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,68 @@
1+
import nunjucks from "nunjucks";
2+
import path from "path";
3+
4+
export function renderTemplate(template, data) {
5+
nunjucks.configure(path.join(process.cwd(), "src/templates"), {
6+
autoescape: true,
7+
});
8+
return nunjucks.render(template, data);
9+
}
110

211
export const COLORS = {
3-
NEWBIE: "#8b898b",
4-
PUPIL: "#0fdb0f",
5-
SPECIALIST: "#55d3ab",
6-
EXPERT: "#2e2eff",
7-
CANDIDATE_MASTER: "#fc3dff",
8-
MASTER: "#ffbd66",
9-
INTERNATIONAL_MASTER: "#ffaf38",
10-
GRANDMASTER: "#fe5858",
11-
INTERNATIONAL_GRANDMASTER: "#ff0000",
12+
NEWBIE: "#8b898b",
13+
PUPIL: "#0fdb0f",
14+
SPECIALIST: "#55d3ab",
15+
EXPERT: "#2e2eff",
16+
CANDIDATE_MASTER: "#fc3dff",
17+
MASTER: "#ffbd66",
18+
INTERNATIONAL_MASTER: "#ffaf38",
19+
GRANDMASTER: "#fe5858",
20+
INTERNATIONAL_GRANDMASTER: "#ff0000",
1221
};
1322

14-
export function get_color_from_rating(rank){
15-
switch (true) {
16-
case rank<1200:
17-
return COLORS.NEWBIE;
18-
case rank<1400:
19-
return COLORS.PUPIL;
20-
case rank<1600:
21-
return COLORS.SPECIALIST;
22-
case rank<1900:
23-
return COLORS.EXPERT;
24-
case rank<2100:
25-
return COLORS.CANDIDATE_MASTER;
26-
case rank<2300:
27-
return COLORS.MASTER;
28-
case rank<2400:
29-
return COLORS.INTERNATIONAL_MASTER;
30-
case rank<2600:
31-
return COLORS.GRANDMASTER;
32-
default:
33-
return COLORS.INTERNATIONAL_GRANDMASTER;
34-
}
35-
36-
};
23+
export function get_color_from_rating(rank) {
24+
switch (true) {
25+
case rank < 1200:
26+
return COLORS.NEWBIE;
27+
case rank < 1400:
28+
return COLORS.PUPIL;
29+
case rank < 1600:
30+
return COLORS.SPECIALIST;
31+
case rank < 1900:
32+
return COLORS.EXPERT;
33+
case rank < 2100:
34+
return COLORS.CANDIDATE_MASTER;
35+
case rank < 2300:
36+
return COLORS.MASTER;
37+
case rank < 2400:
38+
return COLORS.INTERNATIONAL_MASTER;
39+
case rank < 2600:
40+
return COLORS.GRANDMASTER;
41+
default:
42+
return COLORS.INTERNATIONAL_GRANDMASTER;
43+
}
44+
}
3745

3846
export const CONSTANTS = {
39-
THIRTY_MINUTES: 1800,
40-
TWO_HOURS: 7200,
41-
FOUR_HOURS: 14400,
42-
ONE_DAY: 86400,
43-
};
47+
THIRTY_MINUTES: 1800,
48+
TWO_HOURS: 7200,
49+
FOUR_HOURS: 14400,
50+
ONE_DAY: 86400,
51+
};
4452

4553
export const clamp_value = (number, min, max) => {
46-
if (Number.isNaN(parseInt(number))) return min;
47-
return Math.max(min, Math.min(number, max));
54+
if (Number.isNaN(parseInt(number))) return min;
55+
return Math.max(min, Math.min(number, max));
4856
};
4957

5058
export const capitalize = (str) => {
51-
const words = str.split(" ");
52-
const capitalized_words = words.map(word => word.charAt(0).toUpperCase() + word.slice(1));
53-
return capitalized_words.join(" ");
54-
};
59+
const words = str.split(" ");
60+
const capitalized_words = words.map(
61+
(word) => word.charAt(0).toUpperCase() + word.slice(1)
62+
);
63+
return capitalized_words.join(" ");
64+
};
5565

56-
export const word_count = (str) => {
57-
return str.split(" ").length;
58-
}
66+
export const word_count = (str) => {
67+
return str.split(" ").length;
68+
};

src/fetcher-utils.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Axios from "axios";
2+
import { setupCache } from "axios-cache-interceptor";
3+
4+
const instance = Axios.create({
5+
baseURL: "https://codeforces.com/api",
6+
headers: {
7+
"User-Agent": "Codeforces Readme Stats",
8+
},
9+
});
10+
11+
export const api = setupCache(instance);
12+
export const last_rating_cache = new Map();
13+
export const last_stats_cache = new Map();

src/fetcher.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { capitalize } from "@/common.js";
2+
import { api, last_rating_cache, last_stats_cache } from "@/fetcher-utils.js";
3+
4+
function fetch_error_handler(fetch, username, last_cache) {
5+
return new Promise((resolve, reject) => {
6+
const timeoutID = setTimeout(function () {
7+
const res = last_cache.get(username);
8+
if (res != null) resolve(res);
9+
else reject({ status: 500, error: "Codeforces Server Error" });
10+
}, 2000);
11+
fetch()
12+
.then((result) => {
13+
clearTimeout(timeoutID);
14+
resolve(result);
15+
})
16+
.catch((error) => {
17+
if (error.status === 400){
18+
clearTimeout(timeoutID);
19+
reject(error);
20+
}
21+
});
22+
});
23+
}
24+
25+
function count_submissions(submissions) {
26+
let alreadySolved = {};
27+
let problemID;
28+
let count = 0;
29+
for (const submission of submissions) {
30+
problemID = submission.problem.contestId + "-" + submission.problem.index;
31+
if (submission.verdict == "OK" && !alreadySolved[problemID]) {
32+
count++;
33+
alreadySolved[problemID] = true;
34+
}
35+
}
36+
return count;
37+
}
38+
39+
export function get_rating(username, cache_seconds) {
40+
return fetch_error_handler(
41+
() =>
42+
new Promise((resolve, reject) => {
43+
api
44+
.get(`/user.info?handles=${username}`, {
45+
cache: {
46+
ttl: cache_seconds * 1000,
47+
},
48+
})
49+
.then((response) => {
50+
const res = response.data.result[0].rating || 0;
51+
last_rating_cache.set(username, res);
52+
resolve(res);
53+
})
54+
.catch((error) => {
55+
if (error.response.status === 400)
56+
reject({ status: 400, error: "Codeforces Handle Not Found" });
57+
else reject({ status: 500, error: "Codeforces Server Error" });
58+
});
59+
}),
60+
username,
61+
last_rating_cache
62+
);
63+
}
64+
65+
export function get_stats(username, cache_seconds) {
66+
const apiConfig = {
67+
cache: {
68+
ttl: cache_seconds * 1000,
69+
},
70+
};
71+
return fetch_error_handler(
72+
() =>
73+
new Promise((resolve, reject) => {
74+
Promise.all([
75+
api.get(`/user.info?handles=${username}`, apiConfig),
76+
api.get(`/user.rating?handle=${username}`, apiConfig),
77+
api.get(`/user.status?handle=${username}`, apiConfig),
78+
])
79+
.then((responses) => {
80+
let {
81+
firstName,
82+
lastName,
83+
rating,
84+
rank,
85+
maxRank,
86+
maxRating,
87+
friendOfCount,
88+
contribution,
89+
} = responses[0].data.result[0];
90+
91+
rating = rating ? rating : 0;
92+
maxRating = maxRating ? maxRating : 0;
93+
rank = rank ? capitalize(rank) : "Unrated";
94+
maxRank = maxRank ? capitalize(maxRank) : "Unrated";
95+
96+
const fullName = `${firstName} ${lastName}`
97+
.replace("undefined", "")
98+
.replace("undefined", "")
99+
.trim();
100+
const contestsCount = responses[1].data.result.length;
101+
const problemsSolved = count_submissions(responses[2].data.result);
102+
103+
const res = {
104+
username,
105+
fullName,
106+
rating,
107+
maxRating,
108+
rank,
109+
maxRank,
110+
contestsCount,
111+
problemsSolved,
112+
friendOfCount,
113+
contribution,
114+
};
115+
116+
last_stats_cache.set(username, res);
117+
resolve(res);
118+
})
119+
.catch((error) => {
120+
if (error.response.status === 400)
121+
reject({ status: 400, error: "Codeforces Handle Not Found" });
122+
else reject({ status: 500, error: "Codeforces Server Error" });
123+
});
124+
}),
125+
username,
126+
last_stats_cache
127+
);
128+
}

src/hooks/option.js

Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,41 @@
1-
import { useCallback, useState } from 'react';
2-
import qs from 'fast-querystring';
3-
4-
import Error from "../assets/images/error.svg"
1+
import { useCallback, useState } from "react";
2+
import qs from "fast-querystring";
3+
import Error from "@/images/error.svg";
54

65
const defaultOption = {
7-
username: "redheadphone",
8-
theme: "default",
9-
disable_animations: false,
10-
show_icons: true,
11-
force_username: false,
6+
username: "redheadphone",
7+
theme: "default",
8+
disable_animations: false,
9+
show_icons: true,
10+
force_username: false,
1211
};
1312

1413
const useOption = () => {
15-
const [options, setOptions] = useState(defaultOption);
16-
const [querystring, setQuerystring] = useState(qs.stringify(options));
17-
const [error, setError] = useState(false);
14+
const [options, setOptions] = useState(defaultOption);
15+
const [querystring, setQuerystring] = useState(qs.stringify(options));
16+
const [error, setError] = useState(false);
17+
18+
const getImgUrl = (query = querystring) => {
19+
return error ? Error.src : `/api/card?${query}`;
20+
};
1821

19-
const getImgUrl = (query = querystring) => {
20-
return error?(Error.src):`/api/card?${query}`;
21-
};
22+
const updateQuerystring = () => {
23+
setError(false);
24+
setQuerystring(qs.stringify(options));
25+
};
2226

23-
const updateQuerystring = () => {
24-
setError(false);
25-
setQuerystring(qs.stringify(options));
26-
}
27+
const checkSame = (values) => {
28+
return qs.stringify(values) === querystring;
29+
};
2730

28-
const checkSame = (values) => {
29-
return qs.stringify(values) === querystring;
30-
}
31-
32-
return {
33-
options,
34-
setOptions,
35-
getImgUrl,
36-
setError,
37-
updateQuerystring,
38-
checkSame
39-
};
40-
}
31+
return {
32+
options,
33+
setOptions,
34+
getImgUrl,
35+
setError,
36+
updateQuerystring,
37+
checkSame,
38+
};
39+
};
4140

42-
export default useOption;
41+
export default useOption;
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)