From ebe77530067bb32f0a6cc4022b5b1b334721a587 Mon Sep 17 00:00:00 2001 From: JacobLinCool Date: Mon, 27 Jun 2022 19:29:52 +0800 Subject: [PATCH 1/5] chore: upgrade dep `leetcode-query` --- package.json | 2 +- pnpm-lock.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b350f8e..266f0f1 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ }, "dependencies": { "itty-router": "2.6.1", - "leetcode-query": "0.2.1", + "leetcode-query": "0.2.2", "nano-font": "0.3.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d083563..5987cd9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,7 +12,7 @@ specifiers: eslint-config-prettier: ^8.5.0 itty-router: 2.6.1 jest: 28.1.1 - leetcode-query: 0.2.1 + leetcode-query: 0.2.2 nano-font: 0.3.1 prettier: ^2.7.1 ts-jest: 28.0.5 @@ -22,7 +22,7 @@ specifiers: dependencies: itty-router: 2.6.1 - leetcode-query: 0.2.1 + leetcode-query: 0.2.2 nano-font: 0.3.1 devDependencies: @@ -3048,8 +3048,8 @@ packages: engines: {node: '>=6'} dev: true - /leetcode-query/0.2.1: - resolution: {integrity: sha512-UZW8Njpld/41riZWa7Zix7d+ePfvGZqwPyel345Acy1CsVgVxl/DUhz9FhuzAS2iQWnm5AJo36kVYsOLZlrdYA==} + /leetcode-query/0.2.2: + resolution: {integrity: sha512-hc6XT3S60H2A8+VmGlt7z/xtL6NUv7DeRYgdbc43NANgxcLZJy9Tt5do1Yp/r9W/bXYh2oQ20pRoMel+EQznnQ==} dependencies: node-fetch: 2.6.7 transitivePeerDependencies: From 794ed45c72711b729a1fc7be98f98e272f19f977 Mon Sep 17 00:00:00 2001 From: JacobLinCool Date: Mon, 27 Jun 2022 19:30:15 +0800 Subject: [PATCH 2/5] fix: activity extension line width --- src/core/exts/activity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/exts/activity.ts b/src/core/exts/activity.ts index 3776c6e..7c0aff5 100644 --- a/src/core/exts/activity.ts +++ b/src/core/exts/activity.ts @@ -50,7 +50,7 @@ export function ActivityExtension(generator: Generator): Extension { style: { transform: `translate(0px, 200px)` }, children: [ new Item("line", { - attr: { x1: 10, y1: 0, x2: generator.config.width - 20, y2: 0 }, + attr: { x1: 10, y1: 0, x2: generator.config.width - 10, y2: 0 }, style: { stroke: "var(--bg-1)", "stroke-width": 1 }, }), new Item("text", { From 2a17ae01fc0c37af9d568a0d3dc284fad421a448 Mon Sep 17 00:00:00 2001 From: JacobLinCool Date: Mon, 27 Jun 2022 19:30:43 +0800 Subject: [PATCH 3/5] feat: add contest extension --- src/core/exts/contest.ts | 214 +++++++++++++++++++++++++++++++++++++++ src/core/index.ts | 2 + 2 files changed, 216 insertions(+) create mode 100644 src/core/exts/contest.ts diff --git a/src/core/exts/contest.ts b/src/core/exts/contest.ts new file mode 100644 index 0000000..6e77a92 --- /dev/null +++ b/src/core/exts/contest.ts @@ -0,0 +1,214 @@ +import { ContestInfo, ContestRanking, LeetCode, UserContestInfo } from "leetcode-query"; +import { Generator } from "../card"; +import { Gradient } from "../elements"; +import { Item } from "../item"; +import { Extension } from "../types"; + +export function ContestExtension(generator: Generator): Extension { + const pre_result = new Promise( + (resolve) => { + const lc = new LeetCode(); + lc.once("receive-graphql", async (res) => { + try { + const { data } = (await res.json()) as { data: UserContestInfo }; + const history = data.userContestRankingHistory.filter((x) => x.attended); + + if (history.length === 0) { + resolve(null); + return; + } + + resolve({ ranking: data.userContestRanking, history }); + } catch (e) { + resolve(null); + } + }); + lc.user_contest_info(generator.config.username).catch(() => resolve(null)); + }, + ); + + return async function Contest(generator, data, body, styles) { + const result = await pre_result; + + if (result) { + if (generator.config.height < 400) { + generator.config.height = 400; + } + + const start_time = result.history[0].contest.startTime; + const end_time = result.history[result.history.length - 1].contest.startTime; + const [min_rating, max_rating] = result.history.reduce( + ([min, max], { rating }) => [Math.min(min, rating), Math.max(max, rating)], + [Infinity, -Infinity], + ); + + const width = generator.config.width - 90; + const height = 100; + const x_scale = width / (end_time - start_time); + const y_scale = height / (max_rating - min_rating); + + const points = result.history.map((d) => { + const { rating } = d; + const time = d.contest.startTime; + const x = (time - start_time) * x_scale; + const y = (max_rating - rating) * y_scale; + return [x, y]; + }); + + const extension = new Item("g", { + id: "ext-contest", + style: { transform: `translate(0px, 200px)` }, + children: [ + new Item("line", { + attr: { x1: 10, y1: 0, x2: generator.config.width - 10, y2: 0 }, + style: { stroke: "var(--bg-1)", "stroke-width": 1 }, + }), + new Item("text", { + content: "Contest Rating", + id: "ext-contest-rating-title", + style: { + transform: `translate(20px, 20px)`, + fill: "var(--text-1)", + "font-size": "0.8rem", + opacity: generator.config.animation !== false ? 0 : 1, + animation: + generator.config.animation !== false + ? "fade_in 1 0.3s 1.7s forwards" + : "", + }, + }), + new Item("text", { + content: result.ranking.rating.toFixed(0), + id: "ext-contest-rating", + style: { + transform: `translate(20px, 50px)`, + fill: "var(--text-0)", + "font-size": "2rem", + opacity: generator.config.animation !== false ? 0 : 1, + animation: + generator.config.animation !== false + ? "fade_in 1 0.3s 1.7s forwards" + : "", + }, + }), + new Item("text", { + content: "Highest Rating", + id: "ext-contest-highest-rating-title", + style: { + transform: `translate(160px, 20px)`, + fill: "var(--text-1)", + "font-size": "0.8rem", + opacity: generator.config.animation !== false ? 0 : 1, + animation: + generator.config.animation !== false + ? "fade_in 1 0.3s 1.7s forwards" + : "", + }, + }), + new Item("text", { + content: max_rating.toFixed(0), + id: "ext-contest-highest-rating", + style: { + transform: `translate(160px, 50px)`, + fill: "var(--text-0)", + "font-size": "2rem", + opacity: generator.config.animation !== false ? 0 : 1, + animation: + generator.config.animation !== false + ? "fade_in 1 0.3s 1.7s forwards" + : "", + }, + }), + new Item("text", { + content: + result.ranking.globalRanking + " / " + result.ranking.totalParticipants, + id: "ext-contest-ranking", + style: { + transform: `translate(${generator.config.width - 20}px, 20px)`, + "text-anchor": "end", + fill: "var(--text-1)", + "font-size": "0.8rem", + opacity: generator.config.animation !== false ? 0 : 1, + animation: + generator.config.animation !== false + ? "fade_in 1 0.3s 1.7s forwards" + : "", + }, + }), + new Item("text", { + content: result.ranking.topPercentage.toFixed(2) + "%", + id: "ext-contest-percentage", + style: { + transform: `translate(${generator.config.width - 20}px, 50px)`, + "text-anchor": "end", + fill: "var(--text-0)", + "font-size": "2rem", + opacity: generator.config.animation !== false ? 0 : 1, + animation: + generator.config.animation !== false + ? "fade_in 1 0.3s 1.7s forwards" + : "", + }, + }), + ], + }); + + for (let i = Math.ceil(min_rating / 100) * 100; i < max_rating; i += 100) { + const y = (max_rating - i) * y_scale; + const text = new Item("text", { + content: i.toFixed(0), + id: "ext-contest-rating-label-" + i, + style: { + transform: `translate(45px, ${y + 73.5}px)`, + "text-anchor": "end", + fill: "var(--text-2)", + "font-size": "0.7rem", + opacity: generator.config.animation !== false ? 0 : 1, + animation: + generator.config.animation !== false + ? "fade_in 1 0.3s 1.7s forwards" + : "", + }, + }); + const line = new Item("line", { + attr: { x1: 0, y1: y, x2: width + 20, y2: y }, + style: { + stroke: "var(--bg-1)", + "stroke-width": 1, + transform: `translate(50px, 70px)`, + opacity: generator.config.animation !== false ? 0 : 1, + animation: + generator.config.animation !== false + ? "fade_in 1 0.3s 1.7s forwards" + : "", + }, + }); + extension.children?.push(text, line); + } + + extension.children?.push( + new Item("polyline", { + id: "ext-contest-polyline", + attr: { + points: points.map(([x, y]) => `${x},${y}`).join(" "), + }, + style: { + transform: `translate(60px, 70px)`, + stroke: "var(--color-0)", + "stroke-width": 2, + "stroke-linecap": "round", + "stroke-linejoin": "round", + fill: "none", + opacity: generator.config.animation !== false ? 0 : 1, + animation: + generator.config.animation !== false + ? "fade_in 1 0.3s 1.7s forwards" + : "", + }, + }), + ); + + body["ext-contest"] = () => extension; + } + }; +} diff --git a/src/core/index.ts b/src/core/index.ts index 7c59d5d..fdd9cae 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -2,6 +2,7 @@ import { MemoryCache } from "./cache"; import { Generator } from "./card"; import { ActivityExtension } from "./exts/activity"; import { AnimationExtension } from "./exts/animation"; +import { ContestExtension } from "./exts/contest"; import { FontExtension } from "./exts/font"; import { RemoteStyleExtension } from "./exts/remote-style"; import { ThemeExtension } from "./exts/theme"; @@ -40,6 +41,7 @@ export { Config, ActivityExtension, AnimationExtension, + ContestExtension, FontExtension, ThemeExtension, RemoteStyleExtension, From 4c542b15fff321860781aea4804c44d168f448d9 Mon Sep 17 00:00:00 2001 From: JacobLinCool Date: Mon, 27 Jun 2022 19:31:23 +0800 Subject: [PATCH 4/5] feat: support contest extension on cf worker --- src/cloudflare-worker/demo/demo.html | 1 + src/cloudflare-worker/sanitize.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/cloudflare-worker/demo/demo.html b/src/cloudflare-worker/demo/demo.html index 16d01ac..77ead68 100644 --- a/src/cloudflare-worker/demo/demo.html +++ b/src/cloudflare-worker/demo/demo.html @@ -26,6 +26,7 @@

LeetCode Stats Card

diff --git a/src/cloudflare-worker/sanitize.ts b/src/cloudflare-worker/sanitize.ts index 4d32c85..c59b79d 100644 --- a/src/cloudflare-worker/sanitize.ts +++ b/src/cloudflare-worker/sanitize.ts @@ -2,6 +2,7 @@ import { ActivityExtension, AnimationExtension, Config, + ContestExtension, FontExtension, RemoteStyleExtension, ThemeExtension, @@ -69,6 +70,8 @@ export function sanitize(config: Record): Config { if (config.ext === "activity" || config.extension === "activity") { sanitized.extensions.push(ActivityExtension); + } else if (config.ext === "contest" || config.extension === "contest") { + sanitized.extensions.push(ContestExtension); } if (config.border) { From 97e23317f50a42d85bf292e2031cca82931dda83 Mon Sep 17 00:00:00 2001 From: JacobLinCool Date: Mon, 27 Jun 2022 19:43:47 +0800 Subject: [PATCH 5/5] docs: add document of contest extension --- README.md | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ea8d9ef..09127c0 100644 --- a/README.md +++ b/README.md @@ -133,11 +133,13 @@ Hide elements on the card, it is a comma-separated list of element ids. Extension, it is a comma-separated list of extension names. -Now there is only a notable extension: `activity`. +Now there is only two notable extension: `activity` and `contest`. + +NOTICE: You can only use one of `activity` and `contest` at a time now, maybe they can be used together in the future. > But actually animation, font, theme, and external stylesheet are all implemented by extensions and enabled by default. -Want to contribute a `contest` or `nyan-cat` extension? PR is welcome! +Want to contribute a `nyan-cat` extension? PR is welcome! ```md ![](https://leetcard.jacoblin.cool/jacoblincool?ext=activity) @@ -145,6 +147,12 @@ Want to contribute a `contest` or `nyan-cat` extension? PR is welcome! [![](https://leetcard.jacoblin.cool/jacoblincool?ext=activity)](https://leetcard.jacoblin.cool/jacoblincool?ext=activity) +```md +![](https://leetcard.jacoblin.cool/lapor?ext=contest) +``` + +[![](https://leetcard.jacoblin.cool/lapor?ext=contest)](https://leetcard.jacoblin.cool/lapor?ext=contest) + #### `cache` (default: `60`) Cache time in seconds. @@ -263,11 +271,15 @@ Some examples: ### Extensions -Now there is only a notable extension: `activity`. +Extension, it is a comma-separated list of extension names. + +Now there is only two notable extension: `activity` and `contest`. + +NOTICE: You can only use one of `activity` and `contest` at a time now, maybe they can be used together in the future. > But actually animation, font, theme, and external stylesheet are all implemented by extensions and enabled by default. -Want to contribute a `contest` or `nyan-cat` extension? PR is welcome! +Want to contribute a `nyan-cat` extension? PR is welcome! #### `activity` @@ -278,3 +290,13 @@ Show your recent submissions. ``` [![Leetcode Stats](https://leetcard.jacoblin.cool/JacobLinCool?ext=activity)](https://leetcard.jacoblin.cool/JacobLinCool?ext=activity) + +#### `contest` + +Show your contest rating history. + +```md +![Leetcode Stats](https://leetcard.jacoblin.cool/lapor?ext=contest) +``` + +[![Leetcode Stats](https://leetcard.jacoblin.cool/lapor?ext=contest)](https://leetcard.jacoblin.cool/lapor?ext=contest)