Skip to content

Commit e2d1d0e

Browse files
committed
moved API calling & processing to fetcher.js
1 parent 8274a46 commit e2d1d0e

19 files changed

+2645
-2813
lines changed

jest.config.js

+8-10
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
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+
transform: {},
3+
testPathIgnorePatterns: ["<rootDir>/node_modules/"],
4+
modulePathIgnorePatterns: ["<rootDir>/node_modules/"],
5+
coveragePathIgnorePatterns: ["<rootDir>/node_modules/"],
6+
moduleNameMapper: {
7+
"^@/(.*)$": "<rootDir>/src/$1",
8+
},
9+
};

package-lock.json

+1,480-1,789
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -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

-9
This file was deleted.

src/common.js

+56-46
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.js

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import Axios from "axios";
2+
import { setupCache } from "axios-cache-interceptor";
3+
import { capitalize } from "@/common.js";
4+
5+
const instance = Axios.create({
6+
baseURL: "https://codeforces.com/api",
7+
headers: {
8+
"User-Agent": "Codeforces Readme Stats",
9+
},
10+
});
11+
const api = setupCache(instance);
12+
13+
function count_submissions(submissions) {
14+
let alreadySolved = {};
15+
let problemID;
16+
let count = 0;
17+
for (const submission of submissions) {
18+
problemID = submission.problem.contestId + "-" + submission.problem.index;
19+
if (submission.verdict == "OK" && !alreadySolved[problemID]) {
20+
count++;
21+
alreadySolved[problemID] = true;
22+
}
23+
}
24+
return count;
25+
}
26+
27+
export function get_rating(username, cache_seconds) {
28+
console.log("get_rating", username, cache_seconds); // to remove
29+
return new Promise((resolve, reject) => {
30+
api
31+
.get(`/user.info?handles=${username}`, {
32+
cache: {
33+
ttl: cache_seconds * 1000,
34+
},
35+
})
36+
.then((response) => {
37+
resolve(response.data.result[0].rating);
38+
})
39+
.catch((error) => {
40+
reject({ status: 404, error: "Codeforces Handle Not Found" });
41+
reject({ status: 500, error: "Codeforces Server Error" });
42+
});
43+
});
44+
}
45+
46+
export function get_stats(username, cache_seconds) {
47+
const apiConfig = {
48+
cache: {
49+
ttl: cache_seconds * 1000,
50+
},
51+
};
52+
return new Promise((resolve, reject) => {
53+
Promise.all([
54+
api.get(`/user.info?handles=${username}`, apiConfig),
55+
api.get(`/user.rating?handle=${username}`, apiConfig),
56+
api.get(`/user.status?handle=${username}`, apiConfig),
57+
])
58+
.then((responses) => {
59+
let {
60+
firstName,
61+
lastName,
62+
rating,
63+
rank,
64+
maxRank,
65+
maxRating,
66+
friendOfCount,
67+
contribution,
68+
} = responses[0].data.result[0];
69+
70+
rating = rating ? rating : 0;
71+
maxRating = maxRating ? maxRating : 0;
72+
rank = rank ? capitalize(rank) : "Unrated";
73+
maxRank = maxRank ? capitalize(maxRank) : "Unrated";
74+
75+
const fullName = `${firstName} ${lastName}`
76+
.replace("undefined", "").replace("undefined", "")
77+
.trim();
78+
const contestsCount = responses[1].data.result.length;
79+
const problemsSolved = count_submissions(responses[2].data.result);
80+
81+
resolve({
82+
username,
83+
fullName,
84+
rating,
85+
maxRating,
86+
rank,
87+
maxRank,
88+
contestsCount,
89+
problemsSolved,
90+
friendOfCount,
91+
contribution,
92+
});
93+
})
94+
.catch((error) => {
95+
if (error.response.status === 400)
96+
reject({ status: 400, error: "Codeforces Handle Not Found" });
97+
else reject({ status: 500, error: "Codeforces Server Error" });
98+
});
99+
});
100+
}

src/hooks/option.js

+32-33
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 "../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;

src/pages/_app.js

+14-12
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
import dynamic from 'next/dynamic'
2-
import Head from 'next/head'
3-
import './styles.css'
1+
import dynamic from "next/dynamic";
2+
import Head from "next/head";
3+
import "./styles.css";
44

55
function App({ Component, pageProps }) {
6-
return <>
7-
<Head>
8-
<title>Codeforces Readme Stats</title>
9-
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
10-
</Head>
11-
<Component {...pageProps} />
12-
</>
6+
return (
7+
<>
8+
<Head>
9+
<title>Codeforces Readme Stats</title>
10+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
11+
</Head>
12+
<Component {...pageProps} />
13+
</>
14+
);
1315
}
1416

1517
export default dynamic(() => Promise.resolve(App), {
16-
ssr: false
17-
})
18+
ssr: false,
19+
});

0 commit comments

Comments
 (0)