Skip to content

Commit b06bdb4

Browse files
authored
Merge pull request #52 from JacobLinCool/feat/contest-extension
feat: contest extension
2 parents 7a0c2c2 + 97e2331 commit b06bdb4

File tree

8 files changed

+252
-10
lines changed

8 files changed

+252
-10
lines changed

README.md

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,18 +133,26 @@ Hide elements on the card, it is a comma-separated list of element ids.
133133

134134
Extension, it is a comma-separated list of extension names.
135135

136-
Now there is only a notable extension: `activity`.
136+
Now there is only two notable extension: `activity` and `contest`.
137+
138+
NOTICE: You can only use one of `activity` and `contest` at a time now, maybe they can be used together in the future.
137139

138140
> But actually animation, font, theme, and external stylesheet are all implemented by extensions and enabled by default.
139141
140-
Want to contribute a `contest` or `nyan-cat` extension? PR is welcome!
142+
Want to contribute a `nyan-cat` extension? PR is welcome!
141143

142144
```md
143145
![](https://leetcard.jacoblin.cool/jacoblincool?ext=activity)
144146
```
145147

146148
[![](https://leetcard.jacoblin.cool/jacoblincool?ext=activity)](https://leetcard.jacoblin.cool/jacoblincool?ext=activity)
147149

150+
```md
151+
![](https://leetcard.jacoblin.cool/lapor?ext=contest)
152+
```
153+
154+
[![](https://leetcard.jacoblin.cool/lapor?ext=contest)](https://leetcard.jacoblin.cool/lapor?ext=contest)
155+
148156
#### `cache` (default: `60`)
149157

150158
Cache time in seconds.
@@ -263,11 +271,15 @@ Some examples:
263271

264272
### Extensions
265273

266-
Now there is only a notable extension: `activity`.
274+
Extension, it is a comma-separated list of extension names.
275+
276+
Now there is only two notable extension: `activity` and `contest`.
277+
278+
NOTICE: You can only use one of `activity` and `contest` at a time now, maybe they can be used together in the future.
267279

268280
> But actually animation, font, theme, and external stylesheet are all implemented by extensions and enabled by default.
269281
270-
Want to contribute a `contest` or `nyan-cat` extension? PR is welcome!
282+
Want to contribute a `nyan-cat` extension? PR is welcome!
271283

272284
#### `activity`
273285

@@ -278,3 +290,13 @@ Show your recent submissions.
278290
```
279291

280292
[![Leetcode Stats](https://leetcard.jacoblin.cool/JacobLinCool?ext=activity)](https://leetcard.jacoblin.cool/JacobLinCool?ext=activity)
293+
294+
#### `contest`
295+
296+
Show your contest rating history.
297+
298+
```md
299+
![Leetcode Stats](https://leetcard.jacoblin.cool/lapor?ext=contest)
300+
```
301+
302+
[![Leetcode Stats](https://leetcard.jacoblin.cool/lapor?ext=contest)](https://leetcard.jacoblin.cool/lapor?ext=contest)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
},
4242
"dependencies": {
4343
"itty-router": "2.6.1",
44-
"leetcode-query": "0.2.1",
44+
"leetcode-query": "0.2.2",
4545
"nano-font": "0.3.1"
4646
},
4747
"devDependencies": {

pnpm-lock.yaml

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

src/cloudflare-worker/demo/demo.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ <h1>LeetCode Stats Card</h1>
2626
<select id="extension">
2727
<option value="" selected>No Extension</option>
2828
<option value="activity">Activity</option>
29+
<option value="contest">Contest</option>
2930
</select>
3031
<div>
3132
<button onclick="preview()">Preview</button>

src/cloudflare-worker/sanitize.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
ActivityExtension,
33
AnimationExtension,
44
Config,
5+
ContestExtension,
56
FontExtension,
67
RemoteStyleExtension,
78
ThemeExtension,
@@ -69,6 +70,8 @@ export function sanitize(config: Record<string, string>): Config {
6970

7071
if (config.ext === "activity" || config.extension === "activity") {
7172
sanitized.extensions.push(ActivityExtension);
73+
} else if (config.ext === "contest" || config.extension === "contest") {
74+
sanitized.extensions.push(ContestExtension);
7275
}
7376

7477
if (config.border) {

src/core/exts/activity.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export function ActivityExtension(generator: Generator): Extension {
5050
style: { transform: `translate(0px, 200px)` },
5151
children: [
5252
new Item("line", {
53-
attr: { x1: 10, y1: 0, x2: generator.config.width - 20, y2: 0 },
53+
attr: { x1: 10, y1: 0, x2: generator.config.width - 10, y2: 0 },
5454
style: { stroke: "var(--bg-1)", "stroke-width": 1 },
5555
}),
5656
new Item("text", {

src/core/exts/contest.ts

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import { ContestInfo, ContestRanking, LeetCode, UserContestInfo } from "leetcode-query";
2+
import { Generator } from "../card";
3+
import { Gradient } from "../elements";
4+
import { Item } from "../item";
5+
import { Extension } from "../types";
6+
7+
export function ContestExtension(generator: Generator): Extension {
8+
const pre_result = new Promise<null | { ranking: ContestRanking; history: ContestInfo[] }>(
9+
(resolve) => {
10+
const lc = new LeetCode();
11+
lc.once("receive-graphql", async (res) => {
12+
try {
13+
const { data } = (await res.json()) as { data: UserContestInfo };
14+
const history = data.userContestRankingHistory.filter((x) => x.attended);
15+
16+
if (history.length === 0) {
17+
resolve(null);
18+
return;
19+
}
20+
21+
resolve({ ranking: data.userContestRanking, history });
22+
} catch (e) {
23+
resolve(null);
24+
}
25+
});
26+
lc.user_contest_info(generator.config.username).catch(() => resolve(null));
27+
},
28+
);
29+
30+
return async function Contest(generator, data, body, styles) {
31+
const result = await pre_result;
32+
33+
if (result) {
34+
if (generator.config.height < 400) {
35+
generator.config.height = 400;
36+
}
37+
38+
const start_time = result.history[0].contest.startTime;
39+
const end_time = result.history[result.history.length - 1].contest.startTime;
40+
const [min_rating, max_rating] = result.history.reduce(
41+
([min, max], { rating }) => [Math.min(min, rating), Math.max(max, rating)],
42+
[Infinity, -Infinity],
43+
);
44+
45+
const width = generator.config.width - 90;
46+
const height = 100;
47+
const x_scale = width / (end_time - start_time);
48+
const y_scale = height / (max_rating - min_rating);
49+
50+
const points = result.history.map((d) => {
51+
const { rating } = d;
52+
const time = d.contest.startTime;
53+
const x = (time - start_time) * x_scale;
54+
const y = (max_rating - rating) * y_scale;
55+
return [x, y];
56+
});
57+
58+
const extension = new Item("g", {
59+
id: "ext-contest",
60+
style: { transform: `translate(0px, 200px)` },
61+
children: [
62+
new Item("line", {
63+
attr: { x1: 10, y1: 0, x2: generator.config.width - 10, y2: 0 },
64+
style: { stroke: "var(--bg-1)", "stroke-width": 1 },
65+
}),
66+
new Item("text", {
67+
content: "Contest Rating",
68+
id: "ext-contest-rating-title",
69+
style: {
70+
transform: `translate(20px, 20px)`,
71+
fill: "var(--text-1)",
72+
"font-size": "0.8rem",
73+
opacity: generator.config.animation !== false ? 0 : 1,
74+
animation:
75+
generator.config.animation !== false
76+
? "fade_in 1 0.3s 1.7s forwards"
77+
: "",
78+
},
79+
}),
80+
new Item("text", {
81+
content: result.ranking.rating.toFixed(0),
82+
id: "ext-contest-rating",
83+
style: {
84+
transform: `translate(20px, 50px)`,
85+
fill: "var(--text-0)",
86+
"font-size": "2rem",
87+
opacity: generator.config.animation !== false ? 0 : 1,
88+
animation:
89+
generator.config.animation !== false
90+
? "fade_in 1 0.3s 1.7s forwards"
91+
: "",
92+
},
93+
}),
94+
new Item("text", {
95+
content: "Highest Rating",
96+
id: "ext-contest-highest-rating-title",
97+
style: {
98+
transform: `translate(160px, 20px)`,
99+
fill: "var(--text-1)",
100+
"font-size": "0.8rem",
101+
opacity: generator.config.animation !== false ? 0 : 1,
102+
animation:
103+
generator.config.animation !== false
104+
? "fade_in 1 0.3s 1.7s forwards"
105+
: "",
106+
},
107+
}),
108+
new Item("text", {
109+
content: max_rating.toFixed(0),
110+
id: "ext-contest-highest-rating",
111+
style: {
112+
transform: `translate(160px, 50px)`,
113+
fill: "var(--text-0)",
114+
"font-size": "2rem",
115+
opacity: generator.config.animation !== false ? 0 : 1,
116+
animation:
117+
generator.config.animation !== false
118+
? "fade_in 1 0.3s 1.7s forwards"
119+
: "",
120+
},
121+
}),
122+
new Item("text", {
123+
content:
124+
result.ranking.globalRanking + " / " + result.ranking.totalParticipants,
125+
id: "ext-contest-ranking",
126+
style: {
127+
transform: `translate(${generator.config.width - 20}px, 20px)`,
128+
"text-anchor": "end",
129+
fill: "var(--text-1)",
130+
"font-size": "0.8rem",
131+
opacity: generator.config.animation !== false ? 0 : 1,
132+
animation:
133+
generator.config.animation !== false
134+
? "fade_in 1 0.3s 1.7s forwards"
135+
: "",
136+
},
137+
}),
138+
new Item("text", {
139+
content: result.ranking.topPercentage.toFixed(2) + "%",
140+
id: "ext-contest-percentage",
141+
style: {
142+
transform: `translate(${generator.config.width - 20}px, 50px)`,
143+
"text-anchor": "end",
144+
fill: "var(--text-0)",
145+
"font-size": "2rem",
146+
opacity: generator.config.animation !== false ? 0 : 1,
147+
animation:
148+
generator.config.animation !== false
149+
? "fade_in 1 0.3s 1.7s forwards"
150+
: "",
151+
},
152+
}),
153+
],
154+
});
155+
156+
for (let i = Math.ceil(min_rating / 100) * 100; i < max_rating; i += 100) {
157+
const y = (max_rating - i) * y_scale;
158+
const text = new Item("text", {
159+
content: i.toFixed(0),
160+
id: "ext-contest-rating-label-" + i,
161+
style: {
162+
transform: `translate(45px, ${y + 73.5}px)`,
163+
"text-anchor": "end",
164+
fill: "var(--text-2)",
165+
"font-size": "0.7rem",
166+
opacity: generator.config.animation !== false ? 0 : 1,
167+
animation:
168+
generator.config.animation !== false
169+
? "fade_in 1 0.3s 1.7s forwards"
170+
: "",
171+
},
172+
});
173+
const line = new Item("line", {
174+
attr: { x1: 0, y1: y, x2: width + 20, y2: y },
175+
style: {
176+
stroke: "var(--bg-1)",
177+
"stroke-width": 1,
178+
transform: `translate(50px, 70px)`,
179+
opacity: generator.config.animation !== false ? 0 : 1,
180+
animation:
181+
generator.config.animation !== false
182+
? "fade_in 1 0.3s 1.7s forwards"
183+
: "",
184+
},
185+
});
186+
extension.children?.push(text, line);
187+
}
188+
189+
extension.children?.push(
190+
new Item("polyline", {
191+
id: "ext-contest-polyline",
192+
attr: {
193+
points: points.map(([x, y]) => `${x},${y}`).join(" "),
194+
},
195+
style: {
196+
transform: `translate(60px, 70px)`,
197+
stroke: "var(--color-0)",
198+
"stroke-width": 2,
199+
"stroke-linecap": "round",
200+
"stroke-linejoin": "round",
201+
fill: "none",
202+
opacity: generator.config.animation !== false ? 0 : 1,
203+
animation:
204+
generator.config.animation !== false
205+
? "fade_in 1 0.3s 1.7s forwards"
206+
: "",
207+
},
208+
}),
209+
);
210+
211+
body["ext-contest"] = () => extension;
212+
}
213+
};
214+
}

src/core/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { MemoryCache } from "./cache";
22
import { Generator } from "./card";
33
import { ActivityExtension } from "./exts/activity";
44
import { AnimationExtension } from "./exts/animation";
5+
import { ContestExtension } from "./exts/contest";
56
import { FontExtension } from "./exts/font";
67
import { RemoteStyleExtension } from "./exts/remote-style";
78
import { ThemeExtension } from "./exts/theme";
@@ -40,6 +41,7 @@ export {
4041
Config,
4142
ActivityExtension,
4243
AnimationExtension,
44+
ContestExtension,
4345
FontExtension,
4446
ThemeExtension,
4547
RemoteStyleExtension,

0 commit comments

Comments
 (0)