├── .github
└── workflows
│ ├── list_update.yml
│ ├── public_data_updater.yml
│ ├── rating_solution_updater.yml
│ └── workflow.yml
├── .gitignore
├── .npmrc
├── LICENSE
├── README.md
├── app
├── (algo)
│ ├── code
│ │ ├── layout.tsx
│ │ └── page.tsx
│ └── utils.tsx
└── (lc)
│ ├── layout.tsx
│ ├── list
│ ├── binary_search
│ │ └── page.tsx
│ ├── bitwise_operations
│ │ └── page.tsx
│ ├── data_structure
│ │ └── page.tsx
│ ├── dynamic_programming
│ │ └── page.tsx
│ ├── graph
│ │ └── page.tsx
│ ├── greedy
│ │ └── page.tsx
│ ├── grid
│ │ └── page.tsx
│ ├── math
│ │ └── page.tsx
│ ├── monotonic_stack
│ │ └── page.tsx
│ ├── slide_window
│ │ └── page.tsx
│ ├── string
│ │ └── page.tsx
│ └── trees
│ │ └── page.tsx
│ ├── page.tsx
│ ├── search
│ └── page.tsx
│ └── zen
│ └── page.tsx
├── components
├── FixedSidebar
│ └── index.tsx
├── GithubBadge
│ └── index.tsx
├── Loading
│ └── index.tsx
├── MoveToTopButton
│ └── index.tsx
├── ProblemCatetory
│ ├── ProblemCategoryList
│ │ └── index.tsx
│ ├── TableOfContent
│ │ └── index.tsx
│ ├── _index.scss
│ └── index.tsx
├── RatingCircle
│ └── index.tsx
├── RatingText
│ └── index.tsx
├── SettingsPanel
│ ├── Sidebar.tsx
│ ├── config.tsx
│ ├── index.tsx
│ └── settingPages
│ │ ├── CustomizeOptions
│ │ ├── OptionsForm.tsx
│ │ ├── Preview.tsx
│ │ └── index.tsx
│ │ └── SyncProgress.tsx
├── ThemeSwitchButton
│ └── index.tsx
├── containers
│ ├── ContestList
│ │ ├── ContestCell
│ │ │ └── index.tsx
│ │ ├── ProblemCell
│ │ │ └── index.tsx
│ │ └── index.tsx
│ ├── List
│ │ ├── MoveToTodoButton
│ │ │ └── index.tsx
│ │ ├── data
│ │ │ ├── binary_search.ts
│ │ │ ├── bitwise_operations.ts
│ │ │ ├── data_structure.ts
│ │ │ ├── dynamic_programming.ts
│ │ │ ├── graph.ts
│ │ │ ├── greedy.ts
│ │ │ ├── grid.ts
│ │ │ ├── math.ts
│ │ │ ├── monotonic_stack.ts
│ │ │ ├── sliding_window.ts
│ │ │ ├── string.ts
│ │ │ └── trees.ts
│ │ └── index.tsx
│ ├── Search
│ │ └── index.tsx
│ └── Zen
│ │ └── index.tsx
├── icons.tsx
├── layouts
│ ├── MainLayout
│ │ └── index.tsx
│ ├── MdxLayout
│ │ └── index.tsx
│ └── Navbar
│ │ └── index.tsx
└── sections
│ ├── Number.tsx
│ ├── bit.mdx
│ ├── dijkstra.mdx
│ ├── mono.mdx
│ ├── segment_tree.mdx
│ ├── sparestable.mdx
│ └── string.mdx
├── contest.json
├── hooks
├── useContests.ts
├── useProgress
│ ├── index.ts
│ ├── useProgressOption.ts
│ └── useQuestProgress.ts
├── useQuestionTags.ts
├── useSolutions.ts
├── useStorage.ts
├── useTOCHighlights.ts
├── useTags.ts
├── useTheme.tsx
└── useZen.ts
├── lc-maker
├── 0x3f_discuss.py
├── README.md
├── discussion.txt
├── hds.txt
├── js
│ ├── README.md
│ ├── index.js
│ └── index.ts
├── leetcode_api.py
├── list.json
├── main.py
├── rating-list.py
├── requirements.txt
└── socre_fillter
│ ├── list.json
│ └── main.py
├── mdx-components.tsx
├── next-env.d.ts
├── next.config.mjs
├── package-lock.json
├── package.json
├── public
├── contest.json
├── favico.svg
├── qtags.json
├── ratings.json
├── solutions.json
├── tags.json
└── zenk.json
├── qtags.json
├── ratings.json
├── screenshot0.png
├── screenshot1.png
├── scss
├── _bs.scss
├── _common.scss
├── _gh.scss
├── _list.scss
├── _search.scss
├── _zen.scss
├── algorithm
│ └── styles.scss
└── styles.scss
├── solutions.json
├── tags.json
├── tsconfig.json
├── utils
├── debounce.ts
├── hash.ts
└── throttle.ts
└── zenk.json
/.github/workflows/list_update.yml:
--------------------------------------------------------------------------------
1 | name: Weekly Problem List Update
2 |
3 | on:
4 | schedule:
5 | - cron: '0 0 * * 0' # 每周一次,周日午夜
6 | workflow_dispatch:
7 | jobs:
8 | run-script:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - name: Checkout repository
13 | uses: actions/checkout@v4
14 |
15 | - name: Set up Python
16 | uses: actions/setup-python@v5
17 | with:
18 | python-version: '3.11'
19 |
20 | - name: Install dependencies
21 | run: |
22 | python -m pip install --upgrade pip
23 | pip install -r requirements.txt
24 | working-directory: ./lc-maker
25 |
26 | - name: Run script
27 | run: python 0x3f_discuss.py --f ./discussion.txt
28 | working-directory: ./lc-maker
29 |
30 | - name: Configure Git
31 | run: |
32 | git config --global user.name 'github-actions[bot]'
33 | git config --global user.email 'github-actions[bot]@users.noreply.github.com'
34 |
35 | - name: Commit changes
36 | run: |
37 | git add .
38 | git diff-index --quiet HEAD || git commit -m "Update problem list"
39 |
40 | - name: Push changes
41 | if: success()
42 | run: git push
43 | env:
44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45 |
--------------------------------------------------------------------------------
/.github/workflows/public_data_updater.yml:
--------------------------------------------------------------------------------
1 | name: Public Data Updater
2 |
3 | on:
4 | schedule:
5 | - cron: "0 9 * * 1" # 每周一 UTC 时间 09:00 / Beijing 时间 17:00 执行
6 | workflow_dispatch:
7 |
8 | jobs:
9 | public-data-updater:
10 | runs-on: ubuntu-latest
11 | environment: lc-maker
12 | env:
13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
14 | MONGO_URI: ${{ secrets.MONGO_URI }}
15 | DB_NAME: ${{ secrets.DB_NAME }}
16 | DB_USER: ${{ secrets.DB_USER }}
17 | DB_PASS: ${{ secrets.DB_PASS }}
18 | steps:
19 | - name: Checkout repository
20 | uses: actions/checkout@v4
21 |
22 | - name: Configure Git
23 | run: |
24 | git config --global user.name 'github-actions[bot]'
25 | git config --global user.email 'github-actions[bot]@users.noreply.github.com'
26 |
27 | - name: Fetching data
28 | run: |
29 | # echo "Operating system:"
30 | uname -a
31 | # download binary from github release
32 | sudo wget -O /usr/bin/lc https://github.com/huxulm/lc-rating/releases/download/lc-maker/lc-maker
33 | sudo chmod +x /usr/bin/lc
34 | lc --latest --exemask 15 --loglevel 5 --out ./public
35 |
36 | - name: Commit changes
37 | run: |
38 | git add .
39 | git diff-index --quiet HEAD || git commit -m "Update public data🎈"
40 |
41 | - name: Push changes
42 | if: success()
43 | run: git push
44 | env:
45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46 |
--------------------------------------------------------------------------------
/.github/workflows/rating_solution_updater.yml:
--------------------------------------------------------------------------------
1 | name: Rating Solution Updater
2 |
3 | on:
4 | schedule:
5 | - cron: "0 10 * * *" # 每天 UTC 时间 10:00 / Beijing 时间 18:00 执行
6 | workflow_dispatch:
7 |
8 | jobs:
9 | rating-solution-updater:
10 | runs-on: ubuntu-latest
11 | environment: lc-maker
12 | env:
13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
14 | MONGO_URI: ${{ secrets.MONGO_URI }}
15 | DB_NAME: ${{ secrets.DB_NAME }}
16 | DB_USER: ${{ secrets.DB_USER }}
17 | DB_PASS: ${{ secrets.DB_PASS }}
18 | steps:
19 | - name: Checkout repository
20 | uses: actions/checkout@v4
21 |
22 | - name: Configure Git
23 | run: |
24 | git config --global user.name 'github-actions[bot]'
25 | git config --global user.email 'github-actions[bot]@users.noreply.github.com'
26 |
27 | - name: Fetching data
28 | run: |
29 | # echo "Operating system:"
30 | uname -a
31 | # download binary from github release
32 | sudo wget -O /usr/bin/lc https://github.com/huxulm/lc-rating/releases/download/lc-maker/lc-maker
33 | sudo chmod +x /usr/bin/lc
34 | lc --latest --exemask 12 --loglevel 5 --out ./public
35 |
36 | - name: Commit changes
37 | run: |
38 | git add .
39 | git diff-index --quiet HEAD || git commit -m "Update solutions🎈"
40 |
41 | - name: Push changes
42 | if: success()
43 | run: git push
44 | env:
45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46 |
--------------------------------------------------------------------------------
/.github/workflows/workflow.yml:
--------------------------------------------------------------------------------
1 | name: Node.js CI
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 | workflow_dispatch:
7 | branches: ["main"]
8 | workflow_run:
9 | workflows:
10 | [
11 | "Weekly Problem List Update",
12 | "Public Data Updater",
13 | "Rating Solution Updater",
14 | ]
15 | types:
16 | - completed
17 |
18 | permissions:
19 | contents: read
20 | pages: write
21 | id-token: write # Ensure this line is included
22 |
23 | concurrency:
24 | group: "pages"
25 | cancel-in-progress: true
26 |
27 | defaults:
28 | run:
29 | shell: bash
30 |
31 | jobs:
32 | build:
33 | # Specify runner + build & upload the static files as an artifact
34 | runs-on: ubuntu-latest
35 | steps:
36 | - uses: actions/checkout@v3
37 | - run: npm install
38 | - run: CI=false npm run build --if-present
39 | - name: Upload artifact
40 | uses: actions/upload-pages-artifact@v3
41 | with:
42 | path: build/
43 |
44 | deploy:
45 | # Add a dependency to the build job
46 | needs: build
47 |
48 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment
49 | permissions:
50 | pages: write # to deploy to Pages
51 | id-token: write # to verify the deployment originates from an appropriate source
52 |
53 | # Deploy to the github-pages environment
54 | environment:
55 | name: github-pages
56 | url: ${{ steps.deployment.outputs.page_url }}
57 |
58 | # Specify runner + deployment step
59 | runs-on: ubuntu-latest
60 | steps:
61 | - name: Deploy to GitHub Pages
62 | id: deployment
63 | uses: actions/deploy-pages@v4
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | .next
13 | /build
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
26 | .idea
27 | .yarn
28 | .yarnrc.yml
29 |
30 | lc-maker/.venv
31 | lc-maker/__pycache__
32 | lc-maker/hds.txt
33 |
34 | .venv
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npmmirror.com/
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022-present, Huxulm and all other contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LC-Rating
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | ## 介绍
10 | 本项目基于[灵茶山艾府](https://leetcode.cn/u/endlesscheng/)的文章[如何科学刷题?](https://leetcode.cn/circle/discuss/RvFUtj/)而构建的一个刷题用网站。 主要使用 **[React](https://react.dev/)** + **[NextJS](https://nextjs.org/)** 构建。
11 |
12 | ## 特性和使用方法
13 | 本项目有4种使用方法:
14 | 1. 力扣竞赛题目列表,含分数展示,可以让想自己mock contest的用户快速直达并了解题目的难度
15 | 2. 难度训练,对不同难度的题目进行了划分,让用户更好的了解自己的水准。算法新手和老手想在力扣周赛上分的都可以使用此功能。此外还添加了进度标注,并可以对进度进行同步。 同时用户可以在设置中选择自己想刷的tag,也可以隐藏tag, 以及选择自己的进度。
16 | 3. 题解搜索, 支持根据题目、题解标题、算法模板名称、标签等过滤,纯本地化+缓存优化,速度飞快。题解链接(来源:[@灵茶山艾府](https://space.bilibili.com/206214))
17 | 4. 整合了灵茶山艾府列出的题单,标注了分数同时也添加了进度标注。用于突击训练特定知识点,掌握常用算法套路。
18 |
19 | ## Screenshot
20 |
21 |

22 |

23 |
24 |
25 | ## 数据来源
26 | - 基础 - 【[leetcode.cn](https://leetcode.cn/)】
27 | - 题目难度 - 【[leetcode_problem_rating](https://raw.githubusercontent.com/zerotrac/leetcode_problem_rating/main/data.json)】
28 |
29 |
--------------------------------------------------------------------------------
/app/(algo)/code/layout.tsx:
--------------------------------------------------------------------------------
1 | import MdxLayout from "@components/layouts/MdxLayout";
2 | import Dijkstra from "@components/sections/dijkstra.mdx";
3 | import MonotoneStack from "@components/sections/mono.mdx";
4 | import SegmentTree from "@components/sections/segment_tree.mdx";
5 | import SparseTable from "@components/sections/sparestable.mdx";
6 | import String from "@components/sections/string.mdx";
7 | import "@scss/algorithm/styles.scss";
8 |
9 | import type { Metadata } from "next";
10 |
11 | export interface Route {
12 | path: string;
13 | display: string;
14 | mdx: React.ReactNode;
15 | }
16 |
17 | const routes: Route[] = [
18 | {
19 | path: "/algorithm-templates#String",
20 | display: "字符串 (String)",
21 | mdx: ,
22 | },
23 | {
24 | path: "/algorithm-templates#Monotone-Stack",
25 | display: "单调栈 (Monotone Stack)",
26 | mdx: ,
27 | },
28 | {
29 | path: "/algorithm-templates#Dijkstra",
30 | display: "Dijkstra",
31 | mdx: ,
32 | },
33 | {
34 | path: "/algorithm-templates#SparseTable",
35 | display: "SparseTable",
36 | mdx: ,
37 | },
38 | {
39 | path: "/algorithm-templates#SegmentTree",
40 | display: "SegmentTree",
41 | mdx: ,
42 | },
43 | ];
44 |
45 | export const metadata: Metadata = {
46 | title: "My Code Templates",
47 | icons: "/lc-rating/favico.svg",
48 | };
49 |
50 | export default function RootLayout({
51 | children,
52 | }: {
53 | children: React.ReactNode;
54 | }) {
55 | return (
56 |
57 |
58 | {children}
59 |
60 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/app/(algo)/code/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | // @ts-ignore
4 | export default function Algorithm() {
5 | return <>>;
6 | }
7 |
--------------------------------------------------------------------------------
/app/(algo)/utils.tsx:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import path from "path";
3 |
4 | interface Metadata {
5 | title: string;
6 | publishedAt: string;
7 | summary: string;
8 | image?: string;
9 | }
10 |
11 | function parseFrontmatter(fileContent: string) {
12 | let frontmatterRegex = /---\s*([\s\S]*?)\s*---/;
13 | let match = frontmatterRegex.exec(fileContent);
14 | let frontMatterBlock = match![1];
15 | let content = fileContent.replace(frontmatterRegex, "").trim();
16 | let frontMatterLines = frontMatterBlock.trim().split("\n");
17 | let metadata: Partial = {};
18 |
19 | frontMatterLines.forEach((line) => {
20 | let [key, ...valueArr] = line.split(": ");
21 | let value = valueArr.join(": ").trim();
22 | value = value.replace(/^['"](.*)['"]$/, "$1"); // Remove quotes
23 | metadata[key.trim() as keyof Metadata] = value;
24 | });
25 |
26 | return { metadata: metadata as Metadata, content };
27 | }
28 |
29 | function getMDXFiles(dir: string) {
30 | return fs.readdirSync(dir).filter((file) => path.extname(file) === ".mdx");
31 | }
32 |
33 | function readMDXFile(filePath: string) {
34 | let rawContent = fs.readFileSync(filePath, "utf-8");
35 | return parseFrontmatter(rawContent);
36 | }
37 |
38 | function getMDXData(dir: string) {
39 | let mdxFiles = getMDXFiles(dir);
40 | return mdxFiles.map((file) => {
41 | let { metadata, content } = readMDXFile(path.join(dir, file));
42 | let slug = path.basename(file, path.extname(file));
43 |
44 | return {
45 | metadata,
46 | slug,
47 | content,
48 | };
49 | });
50 | }
51 |
52 | export function getBlogPosts() {
53 | return getMDXData(path.join(process.cwd(), "app", "blog", "posts"));
54 | }
55 |
56 | export function formatDate(date: string, includeRelative = false) {
57 | let currentDate = new Date();
58 | if (!date.includes("T")) {
59 | date = `${date}T00:00:00`;
60 | }
61 | let targetDate = new Date(date);
62 |
63 | let yearsAgo = currentDate.getFullYear() - targetDate.getFullYear();
64 | let monthsAgo = currentDate.getMonth() - targetDate.getMonth();
65 | let daysAgo = currentDate.getDate() - targetDate.getDate();
66 |
67 | let formattedDate = "";
68 |
69 | if (yearsAgo > 0) {
70 | formattedDate = `${yearsAgo}y ago`;
71 | } else if (monthsAgo > 0) {
72 | formattedDate = `${monthsAgo}mo ago`;
73 | } else if (daysAgo > 0) {
74 | formattedDate = `${daysAgo}d ago`;
75 | } else {
76 | formattedDate = "Today";
77 | }
78 |
79 | let fullDate = targetDate.toLocaleString("en-us", {
80 | month: "long",
81 | day: "numeric",
82 | year: "numeric",
83 | });
84 |
85 | if (!includeRelative) {
86 | return fullDate;
87 | }
88 |
89 | return `${fullDate} (${formattedDate})`;
90 | }
91 |
--------------------------------------------------------------------------------
/app/(lc)/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import dynamic from "next/dynamic";
3 |
4 | const MainLayout = dynamic(() => import("@components/layouts/MainLayout"), {
5 | ssr: false,
6 | });
7 |
8 | export const metadata: Metadata = {
9 | title: "LC-Rating & Training",
10 | icons: "/lc-rating/favico.svg",
11 | };
12 |
13 | export default function RootLayout({
14 | children,
15 | }: {
16 | children: React.ReactNode;
17 | }) {
18 | return (
19 |
20 |
21 | {children}
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/app/(lc)/list/binary_search/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import data from "@components/containers/List/data/binary_search";
4 | import { lazy } from "react";
5 |
6 | const List = lazy(() => import("@components/containers/List"));
7 |
8 | export default function Page() {
9 | return
;
10 | }
11 |
--------------------------------------------------------------------------------
/app/(lc)/list/bitwise_operations/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import data from "@components/containers/List/data/bitwise_operations";
4 | import { lazy } from "react";
5 |
6 | const List = lazy(() => import("@components/containers/List"));
7 |
8 | export default function Page() {
9 | return
;
10 | }
11 |
--------------------------------------------------------------------------------
/app/(lc)/list/data_structure/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import data from "@components/containers/List/data/data_structure";
4 | import { lazy } from "react";
5 |
6 | const List = lazy(() => import("@components/containers/List"));
7 |
8 | export default function Page() {
9 | return
;
10 | }
11 |
--------------------------------------------------------------------------------
/app/(lc)/list/dynamic_programming/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import data from "@components/containers/List/data/dynamic_programming";
4 | import { lazy } from "react";
5 |
6 | const List = lazy(() => import("@components/containers/List"));
7 |
8 | export default function Page() {
9 | return
;
10 | }
11 |
--------------------------------------------------------------------------------
/app/(lc)/list/graph/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import data from "@components/containers/List/data/graph";
4 | import { lazy } from "react";
5 |
6 | const List = lazy(() => import("@components/containers/List"));
7 |
8 | export default function Page() {
9 | return
;
10 | }
11 |
--------------------------------------------------------------------------------
/app/(lc)/list/greedy/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import data from "@components/containers/List/data/greedy";
4 | import { lazy } from "react";
5 |
6 | const List = lazy(() => import("@components/containers/List"));
7 |
8 | export default function Page() {
9 | return
;
10 | }
11 |
--------------------------------------------------------------------------------
/app/(lc)/list/grid/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import data from "@components/containers/List/data/grid";
4 | import { lazy } from "react";
5 |
6 | const List = lazy(() => import("@components/containers/List"));
7 |
8 | export default function Page() {
9 | return
;
10 | }
11 |
--------------------------------------------------------------------------------
/app/(lc)/list/math/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import data from "@components/containers/List/data/math";
4 | import { lazy } from "react";
5 |
6 | const List = lazy(() => import("@components/containers/List"));
7 |
8 | export default function Page() {
9 | return
;
10 | }
11 |
--------------------------------------------------------------------------------
/app/(lc)/list/monotonic_stack/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import data from "@components/containers/List/data/monotonic_stack";
4 | import { lazy } from "react";
5 |
6 | const List = lazy(() => import("@components/containers/List"));
7 |
8 | export default function Page() {
9 | return
;
10 | }
11 |
--------------------------------------------------------------------------------
/app/(lc)/list/slide_window/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import data from "@components/containers/List/data/sliding_window";
4 | import { lazy } from "react";
5 |
6 | const List = lazy(() => import("@components/containers/List"));
7 |
8 | export default function Page() {
9 | return
;
10 | }
11 |
--------------------------------------------------------------------------------
/app/(lc)/list/string/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import data from "@components/containers/List/data/string";
4 | import { lazy } from "react";
5 |
6 | const List = lazy(() => import("@components/containers/List"));
7 |
8 | export default function Page() {
9 | return
;
10 | }
11 |
--------------------------------------------------------------------------------
/app/(lc)/list/trees/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import data from "@components/containers/List/data/trees";
4 | import { lazy } from "react";
5 |
6 | const List = lazy(() => import("@components/containers/List"));
7 |
8 | export default function Page() {
9 | return
;
10 | }
11 |
--------------------------------------------------------------------------------
/app/(lc)/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { lazy } from "react";
4 |
5 | const ContestList = lazy(() => import("@components/containers/ContestList"));
6 |
7 | // function delay(fn: Promise, timeout: number) {
8 | // return new Promise((resolve) => {
9 | // setTimeout(async () => {
10 | // resolve(await fn);
11 | // }, timeout);
12 | // });
13 | // }
14 |
15 | export default function Page() {
16 | return ;
17 | }
18 |
--------------------------------------------------------------------------------
/app/(lc)/search/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { lazy } from "react";
4 |
5 | const Search = lazy(() => import("@components/containers/Search"));
6 |
7 | export default function Page() {
8 | return ;
9 | }
10 |
--------------------------------------------------------------------------------
/app/(lc)/zen/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { lazy } from "react";
4 |
5 | const Zen = lazy(() => import("@components/containers/Zen"));
6 |
7 | export default function Page() {
8 | return ;
9 | }
10 |
--------------------------------------------------------------------------------
/components/FixedSidebar/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { OverlayTrigger, Tooltip } from "react-bootstrap";
3 | import Stack from "react-bootstrap/Stack";
4 |
5 | export interface FixedItem {
6 | id: string;
7 | content: React.ReactNode;
8 | tooltip?: string;
9 | offset?: { x?: string; y?: string };
10 | }
11 |
12 | interface TooltipWrapperProps {
13 | id: string;
14 | children: React.ReactNode;
15 | tooltip?: string;
16 | }
17 |
18 | const TooltipWrapper: React.FC = ({
19 | id,
20 | children,
21 | tooltip,
22 | }) => {
23 | return tooltip ? (
24 | children && (
25 | {tooltip}}
28 | >
29 | {children}
30 |
31 | )
32 | ) : (
33 | <>{children}>
34 | );
35 | };
36 |
37 | interface FixedSidebarProps {
38 | items: FixedItem[];
39 | position?: "top" | "center" | "bottom";
40 | direction?: "vertical" | "horizontal";
41 | initialOffset?: { x: string; y: string };
42 | gap?: number;
43 | className?: string;
44 | style?: React.CSSProperties;
45 | }
46 |
47 | const FixedSidebar: React.FC = ({
48 | items,
49 | position = "center",
50 | direction = "vertical",
51 | initialOffset = { x: "1rem", y: "0" },
52 | gap = 2,
53 | className,
54 | style,
55 | }) => {
56 | const containerPosition = () => {
57 | const baseStyle = {
58 | right: initialOffset.x,
59 | zIndex: 1050,
60 | };
61 |
62 | switch (position) {
63 | case "top":
64 | return { ...baseStyle, top: initialOffset.y };
65 | case "bottom":
66 | return { ...baseStyle, bottom: initialOffset.y };
67 | default: // center
68 | return {
69 | ...baseStyle,
70 | top: "50%",
71 | transform: `translateY(${initialOffset.y})`,
72 | };
73 | }
74 | };
75 |
76 | return (
77 |
83 | {items.map((item) => (
84 |
92 |
93 | {item.content}
94 |
95 |
96 | ))}
97 |
98 | );
99 | };
100 |
101 | export default FixedSidebar;
102 |
--------------------------------------------------------------------------------
/components/GithubBadge/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import axios from "axios";
4 | import React, { CSSProperties, useEffect, useState } from "react";
5 |
6 | import "@scss/_gh.scss";
7 |
8 | const ICONS_MAP = {
9 | aura999:
10 | "https://vaibhav1663.github.io/custumized-google-form/images/stargazers-aura+999.png",
11 | githubsimple:
12 | "https://vaibhav1663.github.io/custumized-google-form/images/git-simple-contrast.png",
13 | github3d:
14 | "https://vaibhav1663.github.io/custumized-google-form/images/git-3d.png",
15 | octocat:
16 | "https://vaibhav1663.github.io/custumized-google-form/images/octocat-simple.png",
17 | octocatcoloured:
18 | "https://vaibhav1663.github.io/custumized-google-form/images/octocat-colored.png",
19 | chad: "https://vaibhav1663.github.io/custumized-google-form/images/chad.png",
20 | gigachad:
21 | "https://vaibhav1663.github.io/custumized-google-form/images/gigachad.png",
22 | oggigachad:
23 | "https://vaibhav1663.github.io/custumized-google-form/images/og-gigachad.png",
24 | socialcredits0:
25 | "https://vaibhav1663.github.io/custumized-google-form/images/stargazers-social-credits-0.png",
26 | "socialcredits-100":
27 | "https://vaibhav1663.github.io/custumized-google-form/images/stargazers-social-credits-100.png",
28 | "socialcredits-200":
29 | "https://vaibhav1663.github.io/custumized-google-form/images/stargazers-social-credits-200.png",
30 | "socialcredits-300":
31 | "https://vaibhav1663.github.io/custumized-google-form/images/stargazers-social-credits-300.png",
32 | socialcredits999:
33 | "https://vaibhav1663.github.io/custumized-google-form/images/stargazers-social-credits+100.png",
34 | communist:
35 | "https://vaibhav1663.github.io/custumized-google-form/images/communist.png",
36 | };
37 |
38 | interface GithubBadgeProps {
39 | url: string;
40 | theme: string;
41 | text: string;
42 | icon: keyof typeof ICONS_MAP;
43 | className: string | undefined;
44 | style: CSSProperties | undefined;
45 | }
46 |
47 | const GithubBadge = (props: GithubBadgeProps) => {
48 | const [starCount, setStarCount] = useState(-1);
49 | useEffect(() => {
50 | // Function to extract repo owner and name from GitHub URL
51 | const getRepoDetailsFromUrl = (url: string) => {
52 | const match = url.match(/github\.com\/([^\/]+)\/([^\/]+)/);
53 | if (match && match.length === 3) {
54 | return { owner: match[1], repo: match[2] };
55 | } else {
56 | throw new Error("Invalid GitHub repository URL");
57 | }
58 | };
59 |
60 | try {
61 | // Extract owner and repo name from the provided URL
62 | const { owner, repo } = getRepoDetailsFromUrl(props.url);
63 |
64 | // Construct the API URL
65 | const apiUrl = `https://api.github.com/repos/${owner}/${repo}`;
66 |
67 | // Configuration for the API request
68 | let config = {
69 | method: "get",
70 | maxBodyLength: Infinity,
71 | url: apiUrl,
72 | headers: {},
73 | };
74 |
75 | // Make the API request
76 | axios
77 | .request(config)
78 | .then((response) => {
79 | setStarCount(response.data.stargazers_count);
80 | })
81 | .catch((error) => {
82 | console.error(
83 | `Error fetching data from GitHub API: ` +
84 | (error instanceof Error ? error.message : error)
85 | );
86 | });
87 | } catch (error) {
88 | console.error(
89 | `Error fetching data from GitHub API: ` +
90 | (error instanceof Error ? error.message : error)
91 | );
92 | }
93 | }, [props.url]);
94 |
95 | const formatStarCount = (count: number) => {
96 | if (count >= 1000000) {
97 | return (count / 1000000).toFixed(1) + "M";
98 | } else if (count >= 1000) {
99 | return (count / 1000).toFixed(1) + "K";
100 | } else {
101 | return count;
102 | }
103 | };
104 | const baseClass = "github-star-badge";
105 | const themeClass = props.theme || "";
106 | const customClass = props.className || "";
107 |
108 | return (
109 |
116 |
121 |
128 |
135 | {props.text}
136 |
137 |
145 | {starCount != -1
146 | ? formatStarCount(starCount) + " star" + (starCount > 1 ? "s" : "")
147 | : "Loading..."}
148 |
149 |
150 |
151 | );
152 | };
153 |
154 | const GithubBasicBadge = (props: GithubBadgeProps) => {
155 | const [starCount, setStarCount] = useState(0);
156 | useEffect(() => {
157 | // Function to extract repo owner and name from GitHub URL
158 | const getRepoDetailsFromUrl = (url: string) => {
159 | const match = url.match(/github\.com\/([^\/]+)\/([^\/]+)/);
160 | if (match && match.length === 3) {
161 | return { owner: match[1], repo: match[2] };
162 | } else {
163 | throw new Error("Invalid GitHub repository URL");
164 | }
165 | };
166 |
167 | try {
168 | // Extract owner and repo name from the provided URL
169 | const { owner, repo } = getRepoDetailsFromUrl(props.url);
170 |
171 | // Construct the API URL
172 | const apiUrl = `https://api.github.com/repos/${owner}/${repo}`;
173 |
174 | // Configuration for the API request
175 | let config = {
176 | method: "get",
177 | maxBodyLength: Infinity,
178 | url: apiUrl,
179 | headers: {},
180 | };
181 |
182 | // Make the API request
183 | axios
184 | .request(config)
185 | .then((response) => {
186 | setStarCount(response.data.stargazers_count);
187 | })
188 | .catch((error) => {
189 | console.error(
190 | `Error fetching data from GitHub API: ` +
191 | (error instanceof Error ? error.message : error)
192 | );
193 | });
194 | } catch (error) {
195 | console.error(
196 | `Error fetching data from GitHub API: ` +
197 | (error instanceof Error ? error.message : error)
198 | );
199 | }
200 | }, [props.url]);
201 |
202 | const formatStarCount = (count: number) => {
203 | if (count >= 1000000) {
204 | return (count / 1000000).toFixed(1) + "M";
205 | } else if (count >= 1000) {
206 | return (count / 1000).toFixed(1) + "K";
207 | } else {
208 | return count;
209 | }
210 | };
211 |
212 | const baseClass = "github-star-badge basic";
213 | const themeClass = props.theme || "";
214 | const customClass = props.className || "";
215 |
216 | return (
217 |
221 |

226 |
233 | {props.text}
234 |
235 |
255 |
264 | {formatStarCount(starCount)}
265 |
266 |
267 | );
268 | };
269 |
270 | export { GithubBadge, GithubBasicBadge };
271 |
--------------------------------------------------------------------------------
/components/Loading/index.tsx:
--------------------------------------------------------------------------------
1 | export default function () {
2 | return (
3 |
4 |
81 |
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/components/MoveToTopButton/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { Button } from "react-bootstrap";
3 |
4 | export default function MoveToTopButton() {
5 | const [visible, setVisible] = useState(false);
6 |
7 | useEffect(() => {
8 | setVisible(
9 | document.body.scrollTop > 20 || document.documentElement.scrollTop > 20
10 | );
11 | const handleScroll = () =>
12 | setVisible(
13 | document.body.scrollTop > 20 || document.documentElement.scrollTop > 20
14 | );
15 | window.addEventListener("scroll", handleScroll);
16 | return () => window.removeEventListener("scroll", handleScroll);
17 | }, []);
18 |
19 | const moveToTop = () => window.scrollTo({ top: 0, behavior: "smooth" });
20 |
21 | return (
22 | visible && (
23 |
35 | )
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/components/ProblemCatetory/ProblemCategoryList/index.tsx:
--------------------------------------------------------------------------------
1 | import { ShareIcon } from "@components/icons";
2 | import RatingCircle, { ColorRating } from "@components/RatingCircle";
3 | import {
4 | OptionEntry,
5 | ProgressKeyType,
6 | useProgressOptions,
7 | useQuestProgress,
8 | } from "@hooks/useProgress";
9 | import useStorage from "@hooks/useStorage";
10 | import { hashCode } from "@utils/hash";
11 | import Form from "react-bootstrap/esm/Form";
12 |
13 | const getCols = (l: number) => {
14 | if (l < 12) {
15 | return "";
16 | }
17 | if (l < 20) {
18 | return "col2";
19 | }
20 | return "col3";
21 | };
22 |
23 | const title2id = (title: string) => {
24 | // title: number. title
25 | return title.split(". ")[0];
26 | };
27 |
28 | interface ProblemCategory {
29 | title: string;
30 | summary?: string;
31 | src?: string;
32 | original_src?: string;
33 | sort?: Number;
34 | isLeaf?: boolean;
35 | solution?: string | null;
36 | score?: Number | null;
37 | leafChild?: ProblemCategory[];
38 | nonLeafChild?: ProblemCategory[];
39 | isPremium?: boolean;
40 | last_update?: string;
41 | }
42 |
43 | interface ProblemCategoryListProps {
44 | optionKeys: ProgressKeyType[];
45 | getOption: (key?: ProgressKeyType) => OptionEntry;
46 | allProgress: Record;
47 | updateProgress: (questID: string, progress: ProgressKeyType) => void;
48 | removeProgress: (questID: string) => void;
49 | data: ProblemCategory;
50 | showEn?: boolean;
51 | showRating?: boolean;
52 | showPremium?: boolean;
53 | }
54 |
55 | function ProblemCategoryList({
56 | optionKeys,
57 | getOption,
58 | allProgress,
59 | updateProgress,
60 | removeProgress,
61 | data,
62 | showEn,
63 | showRating,
64 | showPremium,
65 | }: ProblemCategoryListProps) {
66 | // Event handlers
67 | const handleProgressSelectChange = (
68 | questID: string,
69 | progress: ProgressKeyType
70 | ) => {
71 | if (progress === getOption().key) {
72 | removeProgress(questID);
73 | } else {
74 | updateProgress(questID, progress);
75 | }
76 | };
77 |
78 | const filteredChild = (data.leafChild || []).filter(
79 | (item) => !item.isPremium || showPremium
80 | );
81 |
82 | return (
83 |
84 |
85 | {data.title}
86 |
87 | {data.summary && (
88 |
92 | )}
93 |
94 | {filteredChild &&
95 | filteredChild.map((item) => {
96 | const id = title2id(item.title);
97 | const progressKey = allProgress[id];
98 | const option = getOption(progressKey);
99 | const rating = Number(item.score);
100 |
101 | return (
102 | -
107 |
124 | {item.score && showRating ? (
125 |
126 |
127 |
128 | {rating.toFixed(0)}
129 |
130 |
131 | ) : null}
132 |
133 |
139 | handleProgressSelectChange(id, e.target.value)
140 | }
141 | >
142 | {optionKeys.map((p) => (
143 |
150 | ))}
151 | {optionKeys.indexOf(option.key) == -1 && (
152 |
159 | )}
160 |
161 |
162 |
163 | );
164 | })}
165 |
166 |
167 | );
168 | }
169 |
170 | export default ProblemCategoryList;
171 |
--------------------------------------------------------------------------------
/components/ProblemCatetory/TableOfContent/index.tsx:
--------------------------------------------------------------------------------
1 | export interface TOC {
2 | id: string;
3 | title: string;
4 | children?: TOC[];
5 | className?: string;
6 | level: number;
7 | count: number;
8 | }
9 |
10 | export const TableOfContent: React.FC<{ toc: TOC }> = ({ toc }) => {
11 | return (
12 |
13 | { toc.title != "介绍" ?
14 | {toc.title} [{toc.count}]
15 | : <>>}
16 | {toc.children?.length > 0 && (
17 |
18 | {toc.children.map((child) => (
19 |
20 | ))}
21 |
22 | )}
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/components/ProblemCatetory/_index.scss:
--------------------------------------------------------------------------------
1 | .toc {
2 | display: grid;
3 | position: sticky;
4 | top: 5rem;
5 | grid-area: toc;
6 | overflow-y: auto;
7 | height: calc(100vh - 5rem);
8 | }
9 | .toc-list,
10 | .toc-list ul {
11 | list-style-type: none;
12 | li {
13 | a {
14 | display: flex;
15 | width: 100%;
16 | font-size: 1.2rem;
17 | text-decoration: none;
18 | &:hover {
19 | background: rgb(235, 238, 240);
20 | padding-left: .2rem;
21 | }
22 | }
23 | }
24 | }
25 |
26 | .toc-list {
27 | padding: 0 0.5rem 0 0.5rem;
28 | }
29 |
30 | .toc-list ul {
31 | padding-inline-start: 1ch;
32 | }
33 |
34 | .pb-container {
35 | gap: 1rem;
36 |
37 | .pb-rating-bg {
38 | // border-radius: 16px;
39 | // padding: 0 2px 0 2px;
40 | .rating-text {
41 | font-family: '黑体', '微软雅黑';
42 | font-weight: 900;
43 | font-size: .75rem;
44 | margin-left: -3px;
45 | }
46 | }
47 | .summary {
48 | background: rgb(251, 251, 251);
49 | font-weight: 600;
50 | a {
51 | text-decoration: underline!important;
52 | }
53 | img {
54 | max-width: 400px!important;
55 | }
56 | }
57 |
58 | &.level-0 {
59 | border-radius: 0;
60 | background: transparent;
61 |
62 | &>.title {
63 | color: $gray-900!important;
64 | }
65 | }
66 |
67 | .level-3 {
68 | h3 > p {
69 | font-weight: 900;
70 | }
71 | }
72 |
73 | .level-2,
74 | .level-4 {
75 | display: flex;
76 | flex-flow: row wrap;
77 | gap: 1rem;
78 | background: transparent;
79 | }
80 |
81 | .leaf {
82 | background: rgb(244, 242, 242);
83 | h3 {
84 | font-weight: 800;
85 | }
86 | .list {
87 | background: rgb(251, 236, 209);
88 | list-style: none;
89 | max-width: 100%;
90 | margin-block-end: 0;
91 |
92 | li:nth-child(n) {
93 | margin-bottom: .2rem;
94 | }
95 |
96 | li:last-child {
97 | margin-bottom: 0;
98 | }
99 |
100 | &.col2,
101 | &.col3 {
102 | columns: 2;
103 | column-rule-width: 2px;
104 | column-rule-color: rgb(107, 107, 107);
105 | column-gap: 1cm;
106 | column-rule-style: dashed;
107 | }
108 |
109 | &.col3 {
110 | columns: 3;
111 | }
112 | }
113 |
114 | a {
115 | color: rgb(0, 0, 0);
116 | font-weight: bolder;
117 | text-decoration: underline;
118 | text-underline-offset: 0.25rem;
119 | }
120 |
121 | li {
122 | width: 100%;
123 | }
124 |
125 | }
126 |
127 | border-radius: 15px;
128 |
129 | .title {
130 | color: aliceblue;
131 | }
132 | }
133 |
134 | @include media-breakpoint-down(md) {
135 | .toc {
136 | display: none;
137 | }
138 | .pb-container {
139 | min-width: 100%;
140 |
141 | .leaf {
142 | .list {
143 |
144 | &.col2,
145 | &.col3 {
146 | columns: unset;
147 | }
148 | }
149 | }
150 | }
151 | }
152 |
153 | @include color-mode(light, false) {
154 | .pb-container {
155 | background: white;
156 | }
157 |
158 | .title {
159 | color: $gray-900;
160 | }
161 | }
162 |
163 | @include color-mode(dark, false) {
164 | .toc-list,
165 | .toc-list ul {
166 | li {
167 | a {
168 | &:hover {
169 | background: white;
170 | padding-left: .2rem;
171 | }
172 | }
173 | }
174 | }
175 | .pb-container {
176 | background: rgb(36, 36, 36);
177 | .summary {
178 | a {
179 | color: $blue;
180 | }
181 | }
182 |
183 | &.level-0 {
184 | border-radius: 0;
185 |
186 | &>.title {
187 | color: white !important;
188 | }
189 | }
190 |
191 | .leaf {
192 | background: rgb(30, 29, 29);
193 |
194 | a {
195 | color: white;
196 | }
197 |
198 | .list {
199 | background: rgb(43, 43, 43);
200 | color: white;
201 | }
202 | }
203 | }
204 |
205 | .title {
206 | color: white;
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/components/ProblemCatetory/index.tsx:
--------------------------------------------------------------------------------
1 | import { hashCode } from "@utils/hash";
2 | import ProblemCategoryList from "./ProblemCategoryList";
3 | import { useProgressOptions, useQuestProgress } from "@hooks/useProgress";
4 |
5 | interface ProblemCategory {
6 | title: string;
7 | summary?: string;
8 | src?: string;
9 | original_src?: string;
10 | sort?: Number;
11 | isLeaf?: boolean;
12 | solution?: string | null;
13 | score?: Number | null;
14 | leafChild?: ProblemCategory[];
15 | nonLeafChild?: ProblemCategory[];
16 | isPremium?: boolean;
17 | last_update?: string;
18 | }
19 |
20 | interface ProblemCategoryProps {
21 | title?: string;
22 | summary?: string;
23 | data?: ProblemCategory[];
24 | className?: string;
25 | level?: number;
26 | showEn?: boolean;
27 | showRating?: boolean;
28 | showPremium?: boolean;
29 | }
30 |
31 | function ProblemCategory({
32 | title,
33 | summary,
34 | data,
35 | className = "",
36 | level = 0,
37 | showEn,
38 | showRating,
39 | showPremium,
40 | }: ProblemCategoryProps) {
41 | const { optionKeys, getOption } = useProgressOptions();
42 | const { allProgress, updateProgress, removeProgress } = useQuestProgress();
43 |
44 | return (
45 |
46 | {
47 |
48 |
49 |
50 | }
51 | {summary && (
52 |
56 | )}
57 |
58 | {data &&
59 | data.map((item) => {
60 | let summary = item.leafChild.length == 0 ? item.summary : "";
61 | let title = item.leafChild.length == 0 ? item.title : "";
62 | return (
63 |
64 | {item.leafChild.length > 0 ? (
65 |
77 | ) : <>>}
78 |
88 |
89 | );
90 | })}
91 |
92 |
93 | );
94 | }
95 |
96 | export default ProblemCategory;
97 |
--------------------------------------------------------------------------------
/components/RatingCircle/index.tsx:
--------------------------------------------------------------------------------
1 | import clsx from "clsx";
2 | import React from "react";
3 |
4 | // [1200, 1399] [1400, 1599] [1600, 1899] [1900, 2099] [2100, 2399] [2400, ]
5 | export const COLORS = [
6 | { l: 0, r: 1200, c: "#ffffff" },
7 | { l: 1200, r: 1400, c: `#828282` },
8 | { l: 1400, r: 1600, c: `#4BA59E` },
9 | { l: 1600, r: 1900, c: `#1B01F5` },
10 | { l: 1900, r: 2100, c: `#9B1EA4` },
11 | { l: 2100, r: 2400, c: `#F09235` },
12 | { l: 2400, r: 3000, c: `#EA3323` },
13 | { l: 3000, r: 4000, c: `#EA3323` },
14 | ];
15 |
16 | export const ColorRating = React.memo(
17 | ({
18 | rating,
19 | ...props
20 | }: {
21 | rating: number;
22 | className?: string;
23 | children: any;
24 | }) => {
25 | const { children } = props;
26 | let c = COLORS.findIndex((v) => rating >= v.l && rating < v.r);
27 | const color = c >= 0 ? c : "-1";
28 | return (
29 |
30 | {children}
31 |
32 | );
33 | }
34 | );
35 |
36 | const RatingCircle = React.forwardRef(
37 | ({ as, bsPrefix, variant, size, active, className, ...props }, ref) => {
38 | const { rating = 0 } = props;
39 | let idx = COLORS.findIndex((v) => rating >= v.l && rating < v.r);
40 | let c = COLORS[idx];
41 | let bgPercent = ((rating - c.l) * 100) / (c.r - c.l + 1);
42 | if (rating >= 3000) {
43 | bgPercent = 0;
44 | }
45 | return (
46 |
47 |
56 | {rating >= 3000 && }
57 |
58 | );
59 | }
60 | );
61 |
62 | export default RatingCircle;
63 |
--------------------------------------------------------------------------------
/components/RatingText/index.tsx:
--------------------------------------------------------------------------------
1 | import clsx from "clsx";
2 | import React from "react";
3 | // [1200, 1399] [1400, 1599] [1600, 1899] [1900, 2099] [2100, 2399] [2400, ]
4 | export const COLORS = [
5 | { l: 0, r: 1200, c: "#ffffff" },
6 | { l: 1200, r: 1400, c: `#377e22c0` },
7 | { l: 1400, r: 1600, c: `#4ba59dbe` },
8 | { l: 1600, r: 1900, c: `#3520f2b1` },
9 | { l: 1900, r: 2100, c: `#9b1ea4b2` },
10 | { l: 2100, r: 2400, c: `#f09235ae` },
11 | { l: 2400, r: 3000, c: `#ea3423b8` },
12 | { l: 3000, r: 4000, c: `#ea3423b8` },
13 | ];
14 | const RatingText = React.forwardRef(
15 | ({ as, bsPrefix, variant, size, active, className, ...props }, ref) => {
16 | const { rating = 0 } = props;
17 | let idx = COLORS.findIndex((v) => rating >= v.l && rating < v.r);
18 | let c = COLORS[idx];
19 | let percent = c && ((rating - c.l) * 100) / (c.r - c.l + 1);
20 | if (rating >= 3000) {
21 | percent = 100;
22 | }
23 | return (
24 |
33 | );
34 | }
35 | );
36 |
37 | export default RatingText;
38 |
--------------------------------------------------------------------------------
/components/SettingsPanel/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import { Nav } from "react-bootstrap";
2 | import { SettingTabType } from "./config";
3 |
4 | interface SidebarProps {
5 | tabs: SettingTabType[];
6 | activeTab: string;
7 | onTabChange: (key: string) => void;
8 | }
9 |
10 | const Sidebar = ({ tabs, activeTab, onTabChange }: SidebarProps) => {
11 | return (
12 |
26 | );
27 | };
28 |
29 | export default Sidebar;
30 |
--------------------------------------------------------------------------------
/components/SettingsPanel/config.tsx:
--------------------------------------------------------------------------------
1 | import { BiSolidCustomize } from "react-icons/bi";
2 | import { LuArrowUpDown } from "react-icons/lu";
3 | import CustomizeOptions from "./settingPages/CustomizeOptions";
4 | import SyncProgress from "./settingPages/SyncProgress";
5 |
6 | export type SettingTabType = {
7 | key: string;
8 | title: string;
9 | icon: React.ReactNode;
10 | component: React.ReactNode;
11 | };
12 |
13 | export const setting_tabs: SettingTabType[] = [
14 | {
15 | key: "SyncProgress",
16 | title: "同步题目进度",
17 | icon: ,
18 | component: ,
19 | },
20 | {
21 | key: "CustomizeOptions",
22 | title: "自定义进度选项",
23 | icon: ,
24 | component: ,
25 | },
26 | ];
27 |
--------------------------------------------------------------------------------
/components/SettingsPanel/index.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Button, Col, Container, Modal, Row } from "react-bootstrap";
3 | import { setting_tabs } from "./config";
4 | import Sidebar from "./Sidebar";
5 |
6 | interface SettingsPanelProps {
7 | show: boolean;
8 | onHide: () => void;
9 | }
10 |
11 | const SettingsPanel = ({ show, onHide }: SettingsPanelProps) => {
12 | const [activeTab, setActiveTab] = useState(setting_tabs[0].key);
13 |
14 | const ActiveComponent = setting_tabs.find(
15 | (tab) => tab.key === activeTab
16 | )?.component;
17 |
18 | return (
19 |
26 |
27 | 站点设置
28 |
29 |
30 |
31 |
32 |
33 |
34 |
39 |
40 |
41 |
42 |
43 | {ActiveComponent ? ActiveComponent : "页面配置错误"}
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
54 |
55 |
56 | );
57 | };
58 |
59 | export default SettingsPanel;
60 |
--------------------------------------------------------------------------------
/components/SettingsPanel/settingPages/CustomizeOptions/OptionsForm.tsx:
--------------------------------------------------------------------------------
1 | import { defaultOptions, OptionEntry } from "@hooks/useProgress";
2 | import { useMemo, useState } from "react";
3 | import { Button, Col, Form, Row, Stack } from "react-bootstrap";
4 |
5 | function partition(array: T[], filter: (item: T) => boolean): [T[], T[]] {
6 | return array.reduce(
7 | (acc, item) => {
8 | acc[Number(filter(item))].push(item);
9 | return acc;
10 | },
11 | [[], []] as [T[], T[]]
12 | );
13 | }
14 |
15 | interface OptionsFormProps {
16 | formData: OptionEntry[];
17 | onChange: (formData: OptionEntry[]) => void;
18 | onSubmit: () => void;
19 | }
20 |
21 | function OptionsForm({ formData, onChange, onSubmit }: OptionsFormProps) {
22 | const sortedFormData = useMemo(() => {
23 | const [customEntries, defaultEntries] = partition(
24 | formData,
25 | (item) => item.key in defaultOptions
26 | );
27 | return [...defaultEntries, ...customEntries];
28 | }, [formData]);
29 |
30 | const errors = useMemo(() => {
31 | const existingKeys = new Set();
32 | const errors: string[] = [];
33 | sortedFormData.forEach((item, i) => {
34 | if (item.key === "") {
35 | errors[i] = "Key不能为空";
36 | } else if (existingKeys.has(item.key)) {
37 | errors[i] = "Key不能重复";
38 | } else {
39 | existingKeys.add(item.key);
40 | }
41 | });
42 | return errors;
43 | }, [sortedFormData]);
44 |
45 | const handleFieldChange = (
46 | e: React.ChangeEvent,
47 | idx: number,
48 | field: "key" | "label" | "color"
49 | ) => {
50 | const newData = [...sortedFormData];
51 | newData[idx] = { ...newData[idx], [field]: e.target.value.trim() };
52 | onChange(newData);
53 | };
54 |
55 | const handleRemove = (idx: number) => {
56 | const newData = sortedFormData.filter((item, i) => i !== idx);
57 | onChange(newData);
58 | };
59 |
60 | const addFormRow = () => {
61 | const newEntry: OptionEntry = {
62 | key: "",
63 | label: "",
64 | color: "#000000",
65 | };
66 | onChange([...sortedFormData, newEntry]);
67 | };
68 |
69 | const handleSubmit = (e: React.FormEvent) => {
70 | e.preventDefault();
71 | if (errors.length === 0) {
72 | onSubmit();
73 | }
74 | };
75 |
76 | return (
77 |
138 | );
139 | }
140 |
141 | export default OptionsForm;
142 |
--------------------------------------------------------------------------------
/components/SettingsPanel/settingPages/CustomizeOptions/Preview.tsx:
--------------------------------------------------------------------------------
1 | import { OptionEntry } from "@hooks/useProgress";
2 | import Form from "react-bootstrap/Form";
3 | import FormLabel from "react-bootstrap/FormLabel";
4 |
5 | interface PreviewProps {
6 | options: OptionEntry[];
7 | }
8 |
9 | function Preview({ options }: PreviewProps) {
10 | return (
11 |
12 | 预览
13 | {options.map((option, i) => (
14 |
20 |
23 |
24 | ))}
25 |
26 | );
27 | }
28 |
29 | export default Preview;
30 |
--------------------------------------------------------------------------------
/components/SettingsPanel/settingPages/CustomizeOptions/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | CustomOptionsType,
3 | OptionEntry,
4 | useProgressOptions,
5 | } from "@hooks/useProgress";
6 | import { useMemo, useState } from "react";
7 | import { Col, Container, Row } from "react-bootstrap";
8 | import OptionsFrom from "./OptionsForm";
9 | import Preview from "./Preview";
10 |
11 | function CustomizeOptions() {
12 | const { optionKeys, getOption, updateOptions } = useProgressOptions();
13 |
14 | const savedFormData = useMemo(
15 | () => optionKeys.map(getOption),
16 | [optionKeys, getOption]
17 | );
18 |
19 | const [newFormData, setNewFormData] = useState(() => {
20 | return savedFormData.map((option) => ({
21 | key: option.key,
22 | label: option.label,
23 | color: option.color,
24 | }));
25 | });
26 |
27 | const onSubmit = () => {
28 | const newOptions = newFormData.reduce((acc: CustomOptionsType, item) => {
29 | acc[item.key] = { key: item.key, label: item.label, color: item.color };
30 | return acc;
31 | }, {});
32 | updateOptions(newOptions);
33 | };
34 |
35 | return (
36 |
37 |
38 |
39 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | export default CustomizeOptions;
55 |
--------------------------------------------------------------------------------
/components/SettingsPanel/settingPages/SyncProgress.tsx:
--------------------------------------------------------------------------------
1 | import { ProgressKeyType, useQuestProgress } from "@hooks/useProgress";
2 | import debounce from "@utils/debounce";
3 | import React, { useEffect, useMemo, useState } from "react";
4 | import { Alert, Button, Form } from "react-bootstrap";
5 |
6 | export default function SyncProgress() {
7 | const [syncStatus, setSyncStatus] = useState<
8 | "idle" | "fetched" | "set" | "error"
9 | >("idle");
10 | const [inputData, setInputData] = useState("");
11 | const { allProgress, setAllProgress } = useQuestProgress();
12 |
13 | const allProgressStr = useMemo(
14 | () => JSON.stringify(allProgress, null, 2),
15 | [allProgress]
16 | );
17 |
18 | const onFetchClick = () => {
19 | setInputData(allProgressStr);
20 | setSyncStatus("fetched");
21 | };
22 |
23 | const onSaveClick = () => {
24 | try {
25 | const parsedData = JSON.parse(inputData) as Record<
26 | string,
27 | ProgressKeyType
28 | >;
29 | setAllProgress(parsedData);
30 | setSyncStatus("set");
31 | } catch (error) {
32 | console.error(
33 | `Error handling Set AllProgress: ` +
34 | (error instanceof Error ? error.message : error)
35 | );
36 | setSyncStatus("error");
37 | }
38 | };
39 |
40 | const onCopyClick = () => {
41 | navigator.clipboard.writeText(allProgressStr);
42 | };
43 |
44 | const [windowHeight, setWindowHeight] = useState(window.innerHeight);
45 |
46 | useEffect(() => {
47 | const onResize = debounce(() => {
48 | setWindowHeight(window.innerHeight);
49 | }, 100);
50 | window.addEventListener("resize", onResize);
51 |
52 | return () => {
53 | window.removeEventListener("resize", onResize);
54 | };
55 | }, []);
56 |
57 | return (
58 |
59 |
60 | {syncStatus === "fetched" && (
61 |
62 |
69 |
77 |
78 | )}
79 |
80 | Input Progress Data:
81 | setInputData(e.target.value)}
86 | />
87 |
88 |
91 | {syncStatus === "set" && (
92 |
93 | 题目进度上传成功
94 |
95 | )}
96 | {syncStatus === "error" && (
97 |
98 | 题目进度上传失败
99 |
100 | )}
101 |
102 | );
103 | }
104 |
--------------------------------------------------------------------------------
/components/ThemeSwitchButton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | interface ThemeSwitchButtonProps extends React.SVGProps {
4 | theme: "light" | "dark";
5 | }
6 | export default function ThemeSwitchButton({
7 | width = 16,
8 | height = 16,
9 | theme = "light",
10 | ...props
11 | }: ThemeSwitchButtonProps<{}>) {
12 | return theme == "light" ? (
13 |
22 | ) : (
23 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/components/containers/ContestList/ContestCell/index.tsx:
--------------------------------------------------------------------------------
1 | import useStorage from "@hooks/useStorage";
2 | import React from "react";
3 | import Form from "react-bootstrap/Form";
4 |
5 | const host = `https://leetcode.cn`;
6 |
7 | function openUrl(url: string) {
8 | window.open(url, "_blank");
9 | }
10 |
11 | interface ContestCellProps {
12 | title: string;
13 | titleSlug: string;
14 | }
15 |
16 | function ContestCell({ title, titleSlug }: ContestCellProps) {
17 | const [mark, setMark] = useStorage("__mark", {
18 | defaultValue: "",
19 | });
20 |
21 | let link = `${host}/contest/${titleSlug}`;
22 | const onClick = (e: React.MouseEvent) => {
23 | e.preventDefault();
24 | openUrl(link);
25 | };
26 | const [ck, setCk] = React.useState(mark === titleSlug);
27 | return (
28 |
29 |
30 | {title}
31 |
32 |
33 | {
36 | setCk(e.target.checked);
37 | setMark(e.target.checked ? titleSlug : "");
38 | }}
39 | checked={ck}
40 | />
41 |
42 |
43 | );
44 | }
45 |
46 | export default ContestCell;
47 |
--------------------------------------------------------------------------------
/components/containers/ContestList/ProblemCell/index.tsx:
--------------------------------------------------------------------------------
1 | import RatingCircle, { COLORS } from "@components/RatingCircle";
2 | import { QuestionType } from "@hooks/useContests";
3 | import { SolutionType } from "@hooks/useSolutions";
4 | import clsx from "clsx";
5 | import React, { useEffect, useState } from "react";
6 | import OverlayTrigger from "react-bootstrap/OverlayTrigger";
7 | import Popover from "react-bootstrap/Popover";
8 | import Spinner from "react-bootstrap/Spinner";
9 |
10 | const host = `https://leetcode.cn`;
11 |
12 | function openUrl(url: string) {
13 | window.open(url, "_blank");
14 | }
15 |
16 | interface ProblemCellProps {
17 | question: QuestionType;
18 | solution: SolutionType;
19 | }
20 |
21 | function ProblemCell({ question: que, solution: soln }: ProblemCellProps) {
22 | let link = `${host}/problems/${que.title_slug}`;
23 | const onClick = (e: React.MouseEvent) => {
24 | e.preventDefault();
25 | openUrl(link);
26 | };
27 | let rating = que.rating;
28 | let idx = COLORS.findIndex((v) => rating >= v.l && rating <= v.r);
29 | let placement = `${rating}`;
30 |
31 | const [display, setDisplay] = useState(true);
32 |
33 | useEffect(() => {
34 | setTimeout(() => setDisplay(false), 5000);
35 | });
36 |
37 | return (
38 |
39 |
45 | {/* {`Popover ${placement}`} */}
46 |
50 | 难度: {rating.toFixed(2)}
51 |
52 |
53 | }
54 | >
55 |
56 |
57 |
65 | {que.question_id}.{que.title}
66 |
67 | {soln && (
68 |
69 |
75 |
79 | {soln.solnTitle}
80 |
81 |
82 | }
83 | >
84 |
94 | 🎈
95 |
96 |
97 |
98 | )}
99 | {!soln && display && (
100 |
101 |
102 |
103 | )}
104 |
105 | );
106 | }
107 |
108 | export default ProblemCell;
109 |
--------------------------------------------------------------------------------
/components/containers/List/MoveToTodoButton/index.tsx:
--------------------------------------------------------------------------------
1 | import { RandomIcon, TodoIcon } from "@components/icons";
2 | import React, { useState } from "react";
3 | import { Button } from "react-bootstrap";
4 |
5 | interface MoveToTodoButtonProps {
6 | random?: boolean;
7 | }
8 |
9 | const MoveToTodoButton: React.FC = ({ random }) => {
10 | const [isBlinking, setIsBlinking] = useState(false);
11 |
12 | const handleBlink = (target: HTMLElement) => {
13 | if (!isBlinking) {
14 | target.classList.add("blinking-effect");
15 | setTimeout(() => {
16 | target.classList.remove("blinking-effect");
17 | setIsBlinking(false);
18 | }, 3000);
19 | setIsBlinking(true);
20 | }
21 | };
22 |
23 | const scrollToTodo = () => {
24 | let targetElement: HTMLElement | null = null;
25 | if (random) {
26 | const todoElements: NodeListOf =
27 | document.querySelectorAll("[data-todo=true]");
28 | if (todoElements.length > 0) {
29 | const randomIndex = Math.floor(Math.random() * todoElements.length);
30 | targetElement = todoElements[randomIndex];
31 | }
32 | } else {
33 | targetElement = document.querySelector("[data-todo=true]");
34 | }
35 | if (targetElement) {
36 | const yOffset = window.innerHeight / 2;
37 | window.scrollTo({
38 | top: targetElement.offsetTop - yOffset,
39 | left: 0,
40 | behavior: "smooth",
41 | });
42 | handleBlink(targetElement);
43 | }
44 | };
45 |
46 | return (
47 |
63 | );
64 | };
65 |
66 | export default MoveToTodoButton;
67 |
--------------------------------------------------------------------------------
/components/containers/List/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import FixedSidebar from "@components/FixedSidebar";
4 | import MoveToTopButton from "@components/MoveToTopButton";
5 | import ProblemCategory from "@components/ProblemCatetory";
6 | import {
7 | TableOfContent,
8 | TOC,
9 | } from "@components/ProblemCatetory/TableOfContent";
10 | import useStorage from "@hooks/useStorage";
11 | import { hashCode } from "@utils/hash";
12 | import { useEffect } from "react";
13 | import Container from "react-bootstrap/Container";
14 | import Form from "react-bootstrap/esm/Form";
15 | import MoveToTodoButton from "./MoveToTodoButton";
16 |
17 | const mapCategory2TOC = (
18 | { title, leafChild, nonLeafChild }: ProblemCategory,
19 | level: number
20 | ): TOC => {
21 | let toc = {
22 | id: `#${hashCode(title)}`,
23 | title: title,
24 | level: level,
25 | count: 0,
26 | } as TOC;
27 | toc.count = leafChild?.length || 0;
28 | toc.children = nonLeafChild.map((c) => {
29 | if (c) return mapCategory2TOC(c, level + 1);
30 | return null;
31 | });
32 | toc.children.forEach((t) => {
33 | toc.count += t.count;
34 | });
35 | return toc;
36 | };
37 |
38 | export default function ({ data }: { data: ProblemCategory }) {
39 | const scrollToComponent = () => {
40 | if (window.location.hash) {
41 | let id = window.location.hash.replace("#", "");
42 | const ele = document.getElementById(id);
43 | if (ele) {
44 | ele.scrollIntoView({ behavior: "instant" });
45 | ele.focus();
46 | }
47 | }
48 | };
49 |
50 | useEffect(() => scrollToComponent(), []);
51 |
52 | const settingDefault = {
53 | showEn: true,
54 | showRating: true,
55 | showPremium: true,
56 | };
57 |
58 | const [setting = settingDefault, setSetting] = useStorage(
59 | "lc-rating-list-settings",
60 | {
61 | defaultValue: settingDefault,
62 | }
63 | );
64 |
65 | const buttons = [
66 | {
67 | id: "move-to-top",
68 | content: ,
69 | },
70 | {
71 | id: "move-to-todo",
72 | content: ,
73 | tooltip: "下一题",
74 | },
75 | {
76 | id: "move-to-random-todo",
77 | content: ,
78 | tooltip: "随机下一题",
79 | },
80 | ];
81 |
82 | const switchers = [
83 | {
84 | id: "toggle-tags",
85 | content: (
86 | {
89 | setSetting({ ...setting, showEn: !setting.showEn });
90 | }}
91 | type="switch"
92 | label="英文链接"
93 | />
94 | ),
95 | },
96 | {
97 | id: "toggle-ratings",
98 | content: (
99 | {
102 | setSetting({ ...setting, showRating: !setting.showRating });
103 | }}
104 | type="switch"
105 | label="难度分"
106 | />
107 | ),
108 | },
109 | {
110 | id: "toggle-premiums",
111 | content: (
112 | {
115 | setSetting({ ...setting, showPremium: !setting.showPremium });
116 | }}
117 | type="switch"
118 | label="会员题"
119 | />
120 | ),
121 | },
122 | ];
123 |
124 | return (
125 |
126 |
132 |
140 |
143 |
148 |
来源:${data.original_src} 最近更新: ${data["last_update"]}`}
150 | data={[data]}
151 | showEn={setting.showEn}
152 | showRating={setting.showRating}
153 | showPremium={setting.showPremium}
154 | summary={""}
155 | />
156 |
157 |
158 | );
159 | }
160 |
--------------------------------------------------------------------------------
/components/containers/Search/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import FixedSidebar from "@components/FixedSidebar";
4 | import MoveToTopButton from "@components/MoveToTopButton";
5 | import { useQuestionTags } from "@hooks/useQuestionTags";
6 | import { useSolutions } from "@hooks/useSolutions";
7 | import { useTags } from "@hooks/useTags";
8 | import {
9 | createColumnHelper,
10 | flexRender,
11 | getCoreRowModel,
12 | getPaginationRowModel,
13 | useReactTable,
14 | } from "@tanstack/react-table";
15 | import React, { useMemo, useState } from "react";
16 | import { Button } from "react-bootstrap";
17 | import ButtonGroup from "react-bootstrap/ButtonGroup";
18 | import Col from "react-bootstrap/Col";
19 | import Container from "react-bootstrap/Container";
20 | import Row from "react-bootstrap/Row";
21 | import Spinner from "react-bootstrap/Spinner";
22 |
23 | const LC_HOST = `https://leetcode.cn`;
24 | const columnHelper = createColumnHelper();
25 |
26 | interface filtSolnsType {
27 | idx: number;
28 | questTitle: string;
29 | questLink: string;
30 | tags: string[];
31 | solnTitle: string;
32 | solnLink: string;
33 | }
34 |
35 | interface PaginatedTableProps {
36 | data: filtSolnsType[];
37 | }
38 |
39 | function PaginatedTable({ data }: PaginatedTableProps) {
40 | const renderTags = (tags: string[]) => {
41 | return (
42 |
43 | {tags.map((t) => {
44 | return (
45 |
46 | {t}
47 |
48 | );
49 | })}
50 |
51 | );
52 | };
53 |
54 | const columns = useMemo(
55 | () => [
56 | columnHelper.display({
57 | id: "index",
58 | header: "编号",
59 | cell: ({ row }) => (
60 | {row.original.idx + 1}
61 | ),
62 | }),
63 | columnHelper.accessor("questTitle", {
64 | header: "题目",
65 | cell: ({ row }) => (
66 |
67 | {row.original.questTitle}
68 |
69 | ),
70 | }),
71 | columnHelper.accessor("tags", {
72 | header: "标签",
73 | cell: ({ row }) => renderTags(row.original.tags),
74 | }),
75 | columnHelper.accessor("solnTitle", {
76 | header: "题解",
77 | cell: ({ row }) => (
78 |
79 | {row.original.solnTitle}
80 |
81 | ),
82 | }),
83 | ],
84 | []
85 | );
86 |
87 | const [pagination, setPagination] = useState({
88 | pageIndex: 0,
89 | pageSize: 10,
90 | });
91 |
92 | const table = useReactTable({
93 | data,
94 | columns,
95 | getCoreRowModel: getCoreRowModel(),
96 | getPaginationRowModel: getPaginationRowModel(),
97 | state: {
98 | pagination,
99 | },
100 | onPaginationChange: (pagination) => {
101 | setPagination(pagination);
102 | },
103 | });
104 |
105 | const [curPage, setCurPage] = useState(1);
106 |
107 | const paginationRow = () => {
108 | return (
109 |
110 |
111 |
118 |
119 | 第 {table.getState().pagination.pageIndex + 1} 页 / 共{" "}
120 | {table.getPageCount()} 页
121 |
122 |
129 |
130 |
131 | 跳转至第
132 | {
138 | setCurPage(Number(e.target.value));
139 | }}
140 | />
141 | 页
142 |
150 |
151 |
163 |
164 | );
165 | };
166 |
167 | return (
168 |
169 | {paginationRow()}
170 |
171 |
172 | {table.getHeaderGroups().map((headerGroup) => (
173 |
174 | {headerGroup.headers.map((header) => (
175 |
176 | {flexRender(
177 | header.column.columnDef.header,
178 | header.getContext()
179 | )}
180 | |
181 | ))}
182 |
183 | ))}
184 |
185 |
186 | {table.getRowModel().rows.map((row, rowIndex) => (
187 |
188 | {row.getVisibleCells().map((cell) => {
189 | const context = {
190 | ...cell.getContext(),
191 | rowIndex,
192 | };
193 | return (
194 |
195 | {flexRender(cell.column.columnDef.cell, context)}
196 | |
197 | );
198 | })}
199 |
200 | ))}
201 |
202 |
203 | {paginationRow()}
204 |
205 | );
206 | }
207 |
208 | export default function Search() {
209 | const [filter, setFilter] = useState("");
210 | const onSearchTextChange = (e: React.ChangeEvent) => {
211 | // @ts-ignore
212 | setFilter(e.target.value);
213 | };
214 |
215 | const { solutions, isPending: solLoading } = useSolutions();
216 | const { tags: qtags, isPending: tgLoading } = useQuestionTags(filter);
217 | const { tags } = useTags();
218 |
219 | const [lang, setLang] = useState<"zh" | "en">("zh");
220 | const onChangeLang = () => {
221 | setLang(() => (lang === "en" ? "zh" : "en"));
222 | };
223 |
224 | const [selectedTags, setSelectedTags] = useState>({});
225 | const onSelectTags = (key: string) => {
226 | setSelectedTags({ ...selectedTags, [key]: !!!selectedTags[key] });
227 | };
228 | const onResetTags = () => {
229 | setSelectedTags({});
230 | };
231 |
232 | const filtSolns = useMemo(() => {
233 | const selectedTagIds = Object.keys(selectedTags).filter(
234 | (id) => !!selectedTags[id]
235 | );
236 |
237 | return Object.keys(solutions)
238 | .filter((hash) => {
239 | let sol = solutions[hash];
240 | return (
241 | filter === "" ||
242 | sol.solnTitle.indexOf(filter) != -1 ||
243 | sol.questId.indexOf(filter) != -1 ||
244 | sol.questTitle.indexOf(filter) != -1
245 | );
246 | })
247 | .filter((hash) => {
248 | const tags = qtags[hash]?.[0] || [];
249 | if (selectedTagIds.length == 0) return true;
250 | return tags.some((tag) => selectedTags[tag]);
251 | })
252 | .sort(function (hash_a, hash_b) {
253 | let a = solutions[hash_a];
254 | let b = solutions[hash_b];
255 | return a.solnTime < b.solnTime ? 1 : a.solnTime == b.solnTime ? 0 : -1;
256 | })
257 | .map((key, idx) => {
258 | const soln = solutions[key];
259 | const questLink = `${LC_HOST}/problems/${soln.questSlug}`;
260 | const solnLink = `${LC_HOST}/problems/${soln.questSlug}/solution/${soln.solnSlug}`;
261 | const questTitle = `${soln.questId}. ${soln.questTitle}`;
262 | const tags =
263 | qtags[soln._hash.toString()]?.[lang === "en" ? 0 : 1] || [];
264 | const solnTitle = soln.solnTitle;
265 |
266 | return {
267 | idx,
268 | questTitle,
269 | questLink,
270 | tags,
271 | solnTitle,
272 | solnLink,
273 | };
274 | });
275 | }, [filter, solutions, selectedTags, solLoading]);
276 |
277 | return (
278 |
279 | ,
284 | },
285 | ]}
286 | position="bottom"
287 | initialOffset={{ x: "2rem", y: "2rem" }}
288 | gap={3}
289 | />
290 |
295 |
302 |
303 |
308 | 总数:{filtSolns.length}
309 |
310 |
311 |
312 |
318 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 | {tags.map((tag) => {
333 | return (
334 | onSelectTags(tag[1])}
336 | className="p-1"
337 | key={tag[1]}
338 | >
339 |
344 | {lang === "en" ? tag[1] : tag[2]}
345 |
346 |
347 | );
348 | })}
349 |
350 |
351 |
352 |
353 |
354 | {solLoading && (
355 |
356 |
357 |
358 | )}
359 |
360 |
361 |
362 |
363 | );
364 | }
365 |
--------------------------------------------------------------------------------
/components/icons.tsx:
--------------------------------------------------------------------------------
1 | export const FilterIcon = (props: any) => {
2 | return (
3 |
20 | );
21 | };
22 |
23 | export const ShareIcon = (props: any) => {
24 | return (
25 |
38 | );
39 | };
40 |
41 | export const TodoIcon = (props: any) => {
42 | return (
43 |
56 | );
57 | };
58 |
59 | export const RandomIcon = (props: any) => {
60 | return (
61 |
75 | );
76 | };
77 |
--------------------------------------------------------------------------------
/components/layouts/MainLayout/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Navbar from "@components/layouts/Navbar";
4 | import Loading from "@components/Loading";
5 | import { ThemeProvider } from "@hooks/useTheme";
6 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
7 | import { Suspense } from "react";
8 |
9 | import "@scss/styles.scss";
10 |
11 | const client = new QueryClient();
12 |
13 | export default function ({ children }: { children: React.ReactNode }) {
14 | return (
15 | }>
16 |
17 |
18 |
19 |
20 | {children}
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/components/layouts/MdxLayout/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Route } from "@app/(algo)/code/layout";
4 | import Link from "next/link";
5 | import { useMemo, useState } from "react";
6 |
7 | interface MaxLayoutProps {
8 | children: React.ReactNode;
9 | routes: Route[];
10 | }
11 |
12 | export default function MdxLayout({ children, routes = [] }: MaxLayoutProps) {
13 | const [selected, setSelected] = useState(routes[0].path);
14 | const code = useMemo(
15 | () => routes.find((r) => r.path === selected),
16 | [selected]
17 | );
18 |
19 | const handleClick = (_: React.MouseEvent, r: Route) => {
20 | setSelected(r.path);
21 | };
22 |
23 | return (
24 |
25 |
28 |
45 |
{code?.mdx ?? ""}
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/components/layouts/Navbar/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { GithubBasicBadge as GithubBadge } from "@components/GithubBadge";
4 | import SettingsPanel from "@components/SettingsPanel";
5 | import ThemeSwitchButton from "@components/ThemeSwitchButton";
6 | import { useTheme } from "@hooks/useTheme";
7 | import Link from "next/dist/client/link";
8 | import { useState } from "react";
9 | import { Button, Container, Dropdown, Nav, Navbar } from "react-bootstrap";
10 |
11 | const questList = [
12 | {
13 | title: "滑动窗口",
14 | link: "/list/slide_window",
15 | },
16 | {
17 | title: "二分查找",
18 | link: "/list/binary_search",
19 | },
20 | {
21 | title: "单调栈",
22 | link: "/list/monotonic_stack",
23 | },
24 | {
25 | title: "网格图",
26 | link: "/list/grid",
27 | },
28 |
29 | {
30 | title: "位运算",
31 | link: "/list/bitwise_operations",
32 | },
33 | {
34 | title: "图论算法",
35 | link: "/list/graph",
36 | },
37 | {
38 | title: "动态规划",
39 | link: "/list/dynamic_programming",
40 | },
41 | {
42 | title: "数据结构",
43 | link: "/list/data_structure",
44 | },
45 |
46 | {
47 | title: "数学",
48 | link: "/list/math",
49 | },
50 | {
51 | title: "贪心",
52 | link: "/list/greedy",
53 | },
54 | {
55 | title: "树和二叉树",
56 | link: "/list/trees",
57 | },
58 | {
59 | title: "字符串",
60 | link: "/list/string",
61 | },
62 | ];
63 |
64 | export default function () {
65 | const { theme, toggleTheme } = useTheme();
66 | const [showModal, setShowModal] = useState(false);
67 | const [showDropdown, setShowDropdown] = useState(false);
68 |
69 | const handleOpenModal = () => {
70 | setShowModal(true);
71 | };
72 | const handleCloseModal = () => {
73 | setShowModal(false);
74 | };
75 |
76 | return (
77 |
78 |
79 | 力扣竞赛题目
80 |
81 | {
84 | toggleTheme();
85 | }}
86 | >
87 |
88 |
89 |
93 | {/* */}
94 |
95 |
96 |
97 |
101 |
204 |
205 | 题解来自{" "}
206 |
211 | bilibili@灵茶山艾府
212 | {" "}
213 | 感谢!
214 |
215 | {
218 | toggleTheme();
219 | }}
220 | >
221 |
222 |
223 |
229 | {/* @ts-ignore */}
230 |
236 |
237 |
238 |
239 |
240 | );
241 | }
242 |
--------------------------------------------------------------------------------
/components/sections/Number.tsx:
--------------------------------------------------------------------------------
1 | interface NumberProps {
2 | children: React.ReactNode;
3 | }
4 |
5 | export default function Number(props: NumberProps) {
6 | return {props.children}
;
7 | }
8 |
--------------------------------------------------------------------------------
/components/sections/bit.mdx:
--------------------------------------------------------------------------------
1 | # FenwickTree(树状数组)
2 |
3 | ## 模板一 区间和, 单点更新,区间查询
4 | > 例题 [307. 区域和检索 - 数组可修改](https://leetcode.cn/problems/range-sum-query-mutable/)
5 | ```Python3
6 | class FenwickTree:
7 | def __init__(self, size):
8 | self.size = size
9 | self.tree = [0] * (size + 1)
10 | self.nums = [0] * size
11 |
12 | def update(self, index, v): # 直接替换,如果 v 是 delta 就不需要额外维护 数组?
13 | """
14 | 单点更新,将索引index处的值增加delta
15 | :param index: 要更新的索引 (1-based)
16 | :param delta: 增量
17 | """
18 | delta = v - self.nums[index - 1]
19 | self.nums[index - 1] = v
20 | while index <= self.size:
21 | self.tree[index] += delta
22 | index += index & -index
23 |
24 | def query(self, index):
25 | """
26 | 查询从1到index的前缀和
27 | :param index: 查询的终止索引 (1-based)
28 | :return: 前缀和
29 | """
30 | sum = 0
31 | while index > 0:
32 | sum += self.tree[index]
33 | index -= index & -index
34 | return sum
35 |
36 | def range_query(self, left, right):
37 | """
38 | 查询区间和 [left, right]
39 | :param left: 区间起始索引 (1-based)
40 | :param right: 区间结束索引 (1-based)
41 | :return: 区间和
42 | """
43 | return self.query(right) - self.query(left - 1)
44 |
45 | class NumArray:
46 |
47 | def __init__(self, nums: List[int]):
48 | self.fwk = FenwickTree(len(nums))
49 | for i, x in enumerate(nums):
50 | self.fwk.update(i + 1, x)
51 |
52 | def update(self, index: int, val: int) -> None:
53 | self.fwk.update(index + 1, val)
54 |
55 | def sumRange(self, left: int, right: int) -> int:
56 | return self.fwk.range_query(left + 1, right + 1)
57 | ```
58 |
--------------------------------------------------------------------------------
/components/sections/dijkstra.mdx:
--------------------------------------------------------------------------------
1 | # Dijkstra
2 | ## 模板一
3 | 求最短路
4 | > 例题 [3123. 最短路径中的边](https://leetcode.cn/problems/find-edges-in-shortest-paths/description/)
5 | ```python
6 | class Dijkstra:
7 |
8 | def __init__(self, n: int, edges: List[List[int]]):
9 | self.n = n
10 | self.g = [[] for _ in range(n)]
11 | for u, v, w in edges:
12 | self.g[u].append((v, w))
13 | self.g[v].append((u, w))
14 |
15 | def getDistance(self, start: int, end: int) -> List[int]:
16 | h = [(0, start)]
17 | vis = [False] * self.n
18 | dist = [inf] * self.n
19 | dist[start] = 0
20 | minD = inf
21 | while h:
22 | d, u = heappop(h)
23 | if vis[u]: continue
24 | vis[u] = True
25 | for v, d0 in self.g[u]:
26 | if vis[v]: continue
27 | if dist[v] > d + d0:
28 | dist[v] = d + d0
29 | heappush(h, (d + d0, v))
30 | return dist
31 | ```
32 |
--------------------------------------------------------------------------------
/components/sections/mono.mdx:
--------------------------------------------------------------------------------
1 | # 单调栈 (MonotoneStack)
2 | [视频讲解](https://www.bilibili.com/video/BV1VN411J7S7/)
3 |
4 | ## 模板一
5 | 找到每个元素的下一个更大元素(Next Greater Element)
6 | > 例题 [739. 每日温度](https://leetcode.cn/problems/daily-temperatures/)
7 | ```python
8 | def next_greater_element(nums):
9 | stack = []
10 | result = [-1] * len(nums)
11 | for i in range(len(nums)):
12 | while stack and nums[i] > nums[stack[-1]]:
13 | result[stack.pop()] = nums[i] # 这里存的是元素,也可以直接存下标 i
14 | stack.append(i)
15 | return result
16 | ```
17 | ## 模板二
18 | 找到每个元素的下一个更小元素(Next Smaller Element)
19 | ```python
20 | def next_smaller_element(a: List[int]) -> List[int]:
21 | stack = []
22 | result = [-1] * len(nums)
23 | for i in range(len(nums)):
24 | while stack and nums[i] < nums[stack[-1]]:
25 | result[stack.pop()] = nums[i] # 这里存的是元素,也可以直接存下标 i
26 | stack.append(i)
27 | return result
28 | ```
29 |
30 | ## 模板三
31 | 找到每个元素的前一个更大元素(Previous Greater Element)
32 | ```python
33 | def previous_greater_element(nums):
34 | stack = []
35 | result = [-1] * len(nums)
36 | for i in range(len(nums)-1, -1, -1):
37 | while stack and nums[i] > nums[stack[-1]]:
38 | result[stack.pop()] = nums[i]
39 | stack.append(i)
40 | return result
41 | ```
42 |
43 | ## 模板四
44 | 找到每个元素的前一个更小元素(Previous Smaller Element)
45 | ```python
46 | def previous_smaller_element(nums):
47 | stack = []
48 | result = [-1] * len(nums)
49 | for i in range(len(nums)-1, -1, -1):
50 | while stack and nums[i] < nums[stack[-1]]:
51 | result[stack.pop()] = nums[i]
52 | stack.append(i)
53 | return result
54 | ```
55 | ## 模板五
56 | 找到每个元素的右侧最小的大于等于当前元素的索引
57 | > 例题 [975. 奇偶跳](https://leetcode.cn/problems/odd-even-jump/description/)
58 | ```python
59 | def next_min_larger_element(nums):
60 | n = len(nums)
61 | a = sorted(range(n), key=lambda i: nums[i]) # 下标按元素值升序
62 | result = [None] * n
63 | st = []
64 | for i in a:
65 | while st and st[-1] < i: # i 是 st "最小的更大"
66 | result[st.pop()] = i
67 | st.append(i)
68 | return result
69 | ```
70 |
71 | ## 模板六
72 | 找到每个元素的右侧最大的小于等于当前元素的索引
73 | > 例题 [975. 奇偶跳](https://leetcode.cn/problems/odd-even-jump/description/)
74 | ```python
75 | def next_max_smaller_element(nums):
76 | n = len(nums)
77 | a = sorted(range(n), key=lambda i: -nums[i]) # 下标按元素值降序
78 | result = [None] * n
79 | st = []
80 | for i in a:
81 | while st and st[-1] < i: # i 是 st "最大的更小"
82 | result[st.pop()] = i
83 | st.append(i)
84 | return result
85 | ```
86 |
87 |
--------------------------------------------------------------------------------
/components/sections/segment_tree.mdx:
--------------------------------------------------------------------------------
1 | # SegmentTree(线段树)
2 |
3 | ## 模板一 查询 区间最大/小值, 可修改
4 | > 例题 [2398. 预算内的最多机器人数目](https://leetcode.cn/problems/maximum-number-of-robots-within-budget/)
5 | ```Python3 []
6 | class SegmentTreeMax:
7 | def __init__(self, data):
8 | self.n = len(data)
9 | self.tree = [0] * (2 * self.n)
10 | # 建树
11 | self._build(data)
12 |
13 | def _build(self, data):
14 | # 初始化线段树的叶节点
15 | for i in range(self.n):
16 | self.tree[self.n + i] = data[i]
17 | # 初始化线段树的内部节点
18 | for i in range(self.n - 1, 0, -1):
19 | self.tree[i] = max(self.tree[2 * i], self.tree[2 * i + 1])
20 |
21 | def update(self, index, value):
22 | # 更新叶节点
23 | pos = index + self.n
24 | self.tree[pos] = value
25 | # 更新线段树中的相关节点
26 | while pos > 1:
27 | pos //= 2
28 | self.tree[pos] = max(self.tree[2 * pos], self.tree[2 * pos + 1])
29 |
30 | def query(self, left, right): # 查询区间 [left, right) 的最大值
31 | left += self.n
32 | right += self.n
33 | max_val = -float('inf')
34 | while left < right:
35 | if left % 2 == 1:
36 | max_val = max(max_val, self.tree[left])
37 | left += 1
38 | if right % 2 == 1:
39 | right -= 1
40 | max_val = max(max_val, self.tree[right])
41 | left //= 2
42 | right //= 2
43 | return max_val
44 | ```
45 |
--------------------------------------------------------------------------------
/components/sections/sparestable.mdx:
--------------------------------------------------------------------------------
1 | # Sparse Table(稀疏表)
2 | 可以静态查询 区间最大/小值
3 | > 例题 [2398. 预算内的最多机器人数目](https://leetcode.cn/problems/maximum-number-of-robots-within-budget/)
4 | ```Python3 []
5 | class SparseTable:
6 | def __init__(self, data):
7 | self.n = len(data)
8 | self.log = [0] * (self.n + 1)
9 | self.log[1] = 0
10 | for i in range(2, self.n + 1):
11 | self.log[i] = self.log[i // 2] + 1
12 |
13 | self.k = self.log[self.n] + 1
14 | self.st = [[0] * self.k for _ in range(self.n)]
15 |
16 | for i in range(self.n):
17 | self.st[i][0] = data[i]
18 |
19 | j = 1
20 | while (1 << j) <= self.n:
21 | i = 0
22 | while (i + (1 << j) - 1) < self.n:
23 | self.st[i][j] = max(self.st[i][j - 1], self.st[i + (1 << (j - 1))][j - 1])
24 | i += 1
25 | j += 1
26 |
27 | def query(self, left, right):
28 | j = self.log[right - left + 1]
29 | return max(self.st[left][j], self.st[right - (1 << j) + 1][j])
30 | ```
31 |
--------------------------------------------------------------------------------
/components/sections/string.mdx:
--------------------------------------------------------------------------------
1 | # 字符串
2 | ## 模板一 KMP
3 | 在字符串中查找子串
4 | > 例题 [3036. 匹配模式数组的子数组数目 II](https://leetcode.cn/problems/number-of-subarrays-that-match-a-pattern-ii/)
5 |
6 | > 例题 [3008. 找出数组中的美丽下标 II](https://leetcode.cn/problems/find-beautiful-indices-in-the-given-array-ii/)
7 | ```python
8 | def kmp(self, s: str, pat: str) -> List[int]:
9 | m = len(pat)
10 | pi = [0] * m
11 | c = 0
12 | for i in range(1, m):
13 | v = pat[i]
14 | while c and pat[c] != v:
15 | c = pi[c - 1]
16 | if pat[c] == v:
17 | c += 1
18 | pi[i] = c
19 |
20 | res = []
21 | c = 0
22 | for i, v in enumerate(s):
23 | while c and pat[c] != v:
24 | c = pi[c - 1]
25 | if pat[c] == v:
26 | c += 1
27 | if c == len(pat):
28 | res.append(i - m + 1)
29 | c = pi[c - 1]
30 | return res
31 | ```
32 | ## 模板二 扩展KMP(Z 函数)
33 | $$z[i]$$ 表示 $$s$$ 和 $$s[i:n]$$ 的最长公共前缀长度
34 | ```python
35 | def z_function(s):
36 | n = len(s)
37 | z = [0] * n
38 | l, r = 0, 0
39 | for i in range(1, n):
40 | if i <= r and z[i - l] < r - i + 1:
41 | z[i] = z[i - l]
42 | else:
43 | z[i] = max(0, r - i + 1)
44 | while i + z[i] < n and s[z[i]] == s[i + z[i]]:
45 | z[i] += 1
46 | if i + z[i] - 1 > r:
47 | l = i
48 | r = i + z[i] - 1
49 | return z
50 | ```
51 |
--------------------------------------------------------------------------------
/hooks/useContests.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useTransition } from "react";
2 |
3 | type Quadra = [T, T, T, T];
4 |
5 | export interface QuestionType {
6 | question_id: number;
7 | rating: number;
8 | title: string;
9 | title_slug: string;
10 | _hash: number;
11 | }
12 |
13 | export interface Contest {
14 | ID: number;
15 | StartTime: number;
16 | Contest: string;
17 | TitleSlug: string;
18 | A: QuestionType;
19 | B: QuestionType;
20 | C: QuestionType;
21 | D: QuestionType;
22 | }
23 |
24 | interface ContestType {
25 | id: number;
26 | start_time: number;
27 | title: string;
28 | title_slug: string;
29 | }
30 |
31 | type ContestsResponse = {
32 | company: {};
33 | contest: ContestType;
34 | questions: Quadra;
35 | }[];
36 |
37 | function mapContests(data: ContestsResponse): Contest[] {
38 | return data.map(({ contest, questions }) => {
39 | return {
40 | ID: contest.id,
41 | StartTime: contest.start_time,
42 | Contest: contest.title,
43 | TitleSlug: contest.title_slug,
44 | A: questions[0],
45 | B: questions[1],
46 | C: questions[2],
47 | D: questions[3],
48 | };
49 | });
50 | }
51 |
52 | export function useContests() {
53 | const [isPending, startTransition] = useTransition();
54 | const [contests, setContests] = useState([]);
55 |
56 | useEffect(() => {
57 | fetch(
58 | "/lc-rating/contest.json?t=" + (new Date().getTime() / 100000).toFixed(0)
59 | )
60 | .then((res) => res.json())
61 | .then((result: ContestsResponse) => {
62 | startTransition(() => {
63 | setContests(mapContests(result));
64 | });
65 | });
66 | }, []);
67 |
68 | return { contests, isPending };
69 | }
70 |
--------------------------------------------------------------------------------
/hooks/useProgress/index.ts:
--------------------------------------------------------------------------------
1 | import { defaultOptions, useProgressOptions } from "./useProgressOption";
2 | import useQuestProgress from "./useQuestProgress";
3 |
4 | import type {
5 | CustomOptionsType,
6 | OptionEntry,
7 | ProgressKeyType,
8 | ProgressOptionsType,
9 | } from "./useProgressOption";
10 |
11 | export { defaultOptions, useProgressOptions, useQuestProgress };
12 | export type {
13 | CustomOptionsType,
14 | OptionEntry,
15 | ProgressKeyType,
16 | ProgressOptionsType
17 | };
18 |
--------------------------------------------------------------------------------
/hooks/useProgress/useProgressOption.ts:
--------------------------------------------------------------------------------
1 | import useStorage from "@hooks/useStorage";
2 | import { useCallback, useMemo } from "react";
3 |
4 | const PROGRESS_CONFIG_KEY = "lc-rating-progress-config";
5 |
6 | export type OptionEntry = {
7 | key: string;
8 | label: string;
9 | color: string;
10 | [key: string]: unknown;
11 | };
12 |
13 | export const defaultOptions = {
14 | TODO: {
15 | key: "TODO",
16 | label: "",
17 | color: "#343a40",
18 | },
19 | WORKING: {
20 | key: "WORKING",
21 | label: "攻略中",
22 | color: "#1E90FF",
23 | },
24 | TOO_HARD: {
25 | key: "TOO_HARD",
26 | label: "太难了,不会",
27 | color: "#dc3545",
28 | },
29 | REVIEW_NEEDED: {
30 | key: "REVIEW_NEEDED",
31 | label: "回头复习下",
32 | color: "#fd7e14",
33 | },
34 | AC: {
35 | key: "AC",
36 | label: "过了",
37 | color: "#28a745",
38 | },
39 | } as const;
40 |
41 | type DefaultOptionsType = typeof defaultOptions;
42 | export type CustomOptionsType = Record;
43 | export type ProgressOptionsType = DefaultOptionsType & CustomOptionsType;
44 | export type ProgressKeyType = keyof ProgressOptionsType;
45 |
46 | export function useProgressOptions() {
47 | const [customOptions, setCustomOptions] =
48 | useStorage(PROGRESS_CONFIG_KEY);
49 |
50 | const fullConfig = useMemo(
51 | () => ({
52 | ...defaultOptions,
53 | ...customOptions,
54 | }),
55 | [customOptions]
56 | );
57 |
58 | const optionKeys = useMemo(() => Object.keys(fullConfig), [fullConfig]);
59 |
60 | const getOption = useCallback(
61 | (key?: ProgressKeyType | null) => {
62 | if (!key) {
63 | return defaultOptions.TODO;
64 | }
65 | if (!(key in fullConfig)) {
66 | console.error(`Invalid progress key: ${key}`);
67 | return {
68 | key,
69 | label: `"${key}" 未定义`,
70 | color: "#dc3545",
71 | };
72 | }
73 | return fullConfig[key];
74 | },
75 | [fullConfig]
76 | );
77 |
78 | const updateOptions = (newOptions: CustomOptionsType) => {
79 | const filteredOptions = Object.keys(newOptions).reduce(
80 | (acc: CustomOptionsType, key) => {
81 | if (!key) {
82 | console.error("Key cannot be empty: ", key);
83 | } else if (key in acc) {
84 | console.error("Key cannot be duplicated: ", key);
85 | } else {
86 | acc[key] = newOptions[key];
87 | }
88 | return acc;
89 | },
90 | {}
91 | );
92 | if (
93 | Object.keys(filteredOptions).length !== Object.keys(newOptions).length
94 | ) {
95 | }
96 | setCustomOptions({ ...defaultOptions, ...filteredOptions });
97 | };
98 |
99 | return {
100 | optionKeys,
101 | getOption,
102 | updateOptions,
103 | };
104 | }
105 |
--------------------------------------------------------------------------------
/hooks/useProgress/useQuestProgress.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useSyncExternalStore } from "react";
2 | import { ProgressKeyType } from "./useProgressOption";
3 |
4 | const storageKeyPrefix = "lc-rating-zen-progress-";
5 | const getStorageKey = (questID: string) => `${storageKeyPrefix}${questID}`;
6 |
7 | type QuestProgressType = Record;
8 |
9 | const isBrowser = () => typeof window !== "undefined";
10 |
11 | const getQuestProgressKeys = () => {
12 | const keys = Object.keys(localStorage).filter((key) =>
13 | key.startsWith(storageKeyPrefix)
14 | );
15 | return keys;
16 | };
17 |
18 | interface StoreType {
19 | allProgress: QuestProgressType;
20 | setAllProgress: (newProgress: QuestProgressType) => void;
21 | updateProgress: (questID: string, progress: ProgressKeyType) => void;
22 | removeProgress: (questID: string) => void;
23 |
24 | listeners: Set<() => void>;
25 | subscribe: (listener: () => void) => () => void;
26 | getSnapshot: () => QuestProgressType;
27 | notifyListeners: () => void;
28 | }
29 |
30 | class Store implements StoreType {
31 | allProgress: QuestProgressType;
32 | listeners: Set<() => void>;
33 |
34 | constructor() {
35 | this.allProgress = {};
36 | this.listeners = new Set();
37 |
38 | if (isBrowser()) {
39 | const keys = getQuestProgressKeys();
40 | keys.forEach((key) => {
41 | const value = localStorage.getItem(key);
42 | const questID = key.replace(storageKeyPrefix, "");
43 | if (value) {
44 | this.allProgress[questID] = value as ProgressKeyType;
45 | }
46 | });
47 | }
48 | }
49 |
50 | setAllProgress = (newProgress: QuestProgressType) => {
51 | if (isBrowser()) {
52 | Object.entries(newProgress).forEach(([questID, progress]) => {
53 | const key = getStorageKey(questID);
54 | localStorage.setItem(key, progress);
55 | });
56 | }
57 |
58 | this.allProgress = { ...this.allProgress, ...newProgress };
59 | this.notifyListeners();
60 | };
61 |
62 | updateProgress = (questID: string, progress: ProgressKeyType) => {
63 | if (isBrowser()) {
64 | const key = getStorageKey(questID);
65 | localStorage.setItem(key, progress);
66 | }
67 |
68 | this.allProgress = { ...this.allProgress, [questID]: progress };
69 | this.notifyListeners();
70 | };
71 |
72 | removeProgress = (questID: string) => {
73 | if (isBrowser()) {
74 | const key = getStorageKey(questID);
75 | localStorage.removeItem(key);
76 | }
77 |
78 | const { [questID]: _, ...rest } = this.allProgress;
79 | this.allProgress = rest;
80 | this.notifyListeners();
81 | };
82 |
83 | subscribe = (listener: () => void) => {
84 | this.listeners.add(listener);
85 | return () => this.listeners.delete(listener);
86 | };
87 |
88 | getSnapshot = () => this.allProgress;
89 |
90 | notifyListeners = () => {
91 | this.listeners.forEach((listener) => listener());
92 | };
93 | }
94 |
95 | const store = new Store();
96 |
97 | function useQuestProgress(): {
98 | allProgress: QuestProgressType;
99 | setAllProgress: (newProgress: QuestProgressType) => void;
100 | updateProgress: (questID: string, progress: ProgressKeyType) => void;
101 | removeProgress: (questID: string) => void;
102 | } {
103 | const allProgress = useSyncExternalStore(store.subscribe, store.getSnapshot);
104 |
105 | useEffect(() => {
106 | if (!isBrowser()) {
107 | return;
108 | }
109 |
110 | const handleStorageChange = (e: StorageEvent) => {
111 | if (
112 | e.key?.startsWith(storageKeyPrefix) &&
113 | e.storageArea === localStorage
114 | ) {
115 | const questID = e.key.replace(storageKeyPrefix, "");
116 | const newProgress = e.newValue as ProgressKeyType;
117 | if (newProgress) {
118 | store.updateProgress(questID, newProgress);
119 | } else {
120 | store.removeProgress(questID);
121 | }
122 | }
123 | };
124 |
125 | window.addEventListener("storage", handleStorageChange);
126 | return () => window.removeEventListener("storage", handleStorageChange);
127 | }, []);
128 |
129 | return {
130 | allProgress,
131 | setAllProgress: store.setAllProgress.bind(store),
132 | updateProgress: store.updateProgress.bind(store),
133 | removeProgress: store.removeProgress.bind(store),
134 | };
135 | }
136 |
137 | export default useQuestProgress;
138 |
--------------------------------------------------------------------------------
/hooks/useQuestionTags.ts:
--------------------------------------------------------------------------------
1 | import { useSuspenseQuery } from "@tanstack/react-query";
2 |
3 | export type QTag = [string[], string[]];
4 | export type QTags = Record;
5 |
6 | export function useQuestionTags(filter: any) {
7 | const { data, isFetching } = useSuspenseQuery({
8 | queryKey: ["qtags"],
9 | queryFn: () => {
10 | return fetch(
11 | "/lc-rating/qtags.json?t=" + (new Date().getTime() / 100000).toFixed(0)
12 | )
13 | .then((res) => res.json())
14 | .then((result: QTags) => {
15 | return result;
16 | });
17 | },
18 | refetchOnWindowFocus: false,
19 | });
20 |
21 | return { tags: data, isPending: isFetching };
22 | }
23 |
--------------------------------------------------------------------------------
/hooks/useSolutions.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useTransition } from "react";
2 |
3 | type SolutionsResponse = Record<
4 | string,
5 | [string, string, string, `${number}`, string, string, number]
6 | >;
7 |
8 | export interface SolutionType {
9 | questTitle: string;
10 | questSlug: string;
11 | questId: string;
12 | solnTitle: string;
13 | solnSlug: string;
14 | solnTime: string;
15 | _hash: number;
16 | }
17 |
18 | export type Solutions = Record;
19 |
20 | export function useSolutions() {
21 | // solutions
22 | const [isPending, startTransition] = useTransition();
23 | const [solutions, setSolutions] = useState({});
24 |
25 | useEffect(() => {
26 | fetch(
27 | "/lc-rating/solutions.json?t=" +
28 | (new Date().getTime() / 100000).toFixed(0)
29 | )
30 | .then((res) => res.json())
31 | .then((result: SolutionsResponse) => {
32 | startTransition(() => {
33 | let solutions: Solutions = {};
34 | for (let key in result) {
35 | const [
36 | solnTitle,
37 | solnSlug,
38 | solnTime,
39 | questId,
40 | questTitle,
41 | questSlug,
42 | _hash,
43 | ] = result[key];
44 |
45 | solutions[key] = {
46 | questTitle,
47 | questSlug,
48 | questId,
49 | solnTitle,
50 | solnSlug,
51 | solnTime,
52 | _hash,
53 | };
54 | }
55 | setSolutions(solutions);
56 | });
57 | });
58 | }, []);
59 |
60 | return { solutions, isPending };
61 | }
62 |
--------------------------------------------------------------------------------
/hooks/useStorage.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Dispatch,
3 | SetStateAction,
4 | useCallback,
5 | useSyncExternalStore,
6 | } from "react";
7 |
8 | type StorageType = "local" | "session";
9 |
10 | type Encryption =
11 | | {
12 | encrypt: (data: string) => string;
13 | decrypt: (data: string) => string;
14 | }
15 | | {};
16 |
17 | type Serialization =
18 | | {
19 | serializer: (data: T) => string;
20 | deserializer: (data: string) => T;
21 | }
22 | | {};
23 |
24 | type Options = {
25 | type?: StorageType;
26 | defaultValue?: T;
27 | } & Serialization &
28 | Encryption;
29 |
30 | // 全局存储 StorageStore 实例
31 | const globalStores: Record>> = {
32 | local: {},
33 | session: {},
34 | };
35 |
36 | class StorageStore {
37 | private key: string;
38 | private options: Options;
39 | private listeners: Set<() => void> = new Set();
40 | private cachedValue: T | undefined; // Cache the last snapshot value
41 |
42 | constructor(key: string, options: Options) {
43 | this.key = key;
44 | this.options = options;
45 | this.cachedValue = this.getValue(); // Initialize the cached value
46 | }
47 |
48 | public getStorage(): Storage | undefined {
49 | if (typeof window === "undefined") {
50 | return undefined;
51 | }
52 | return this.options.type === "session"
53 | ? window.sessionStorage
54 | : window.localStorage;
55 | }
56 |
57 | private getValue(): T | undefined {
58 | const storage = this.getStorage();
59 | const value = storage?.getItem(this.key);
60 | if (value === undefined || value === null) {
61 | return this.options.defaultValue;
62 | }
63 | try {
64 | const decryptedValue =
65 | "decrypt" in this.options ? this.options.decrypt(value) : value;
66 | const deserializedValue =
67 | "deserializer" in this.options
68 | ? this.options.deserializer(decryptedValue)
69 | : JSON.parse(decryptedValue);
70 | return deserializedValue;
71 | } catch (error) {
72 | console.error("Failed to parse value from storage: ", error);
73 | return this.options.defaultValue;
74 | }
75 | }
76 |
77 | private setValue(value: T | undefined) {
78 | const storage = this.getStorage();
79 | if (value === undefined || value === null) {
80 | storage?.removeItem(this.key);
81 | } else {
82 | const serializedValue =
83 | "serializer" in this.options
84 | ? this.options.serializer(value)
85 | : JSON.stringify(value);
86 | const encryptedValue =
87 | "encrypt" in this.options
88 | ? this.options.encrypt(serializedValue)
89 | : serializedValue;
90 | storage?.setItem(this.key, encryptedValue);
91 | }
92 | this.cachedValue = value;
93 | this.notifyListeners();
94 | }
95 |
96 | subscribe(listener: () => void): () => void {
97 | const handleStorageChange = (e: StorageEvent) => {
98 | if (e.key === null) return;
99 | if (e.key === this.key && e.storageArea === this.getStorage()) {
100 | this.cachedValue = this.getValue();
101 | this.notifyListeners();
102 | }
103 | };
104 | this.listeners.add(listener);
105 | window.addEventListener("storage", handleStorageChange);
106 |
107 | return () => {
108 | window.removeEventListener("storage", handleStorageChange);
109 | this.listeners.delete(listener);
110 | };
111 | }
112 |
113 | getSnapshot(): T | undefined {
114 | return this.cachedValue;
115 | }
116 |
117 | getServerSnapshot(): T | undefined {
118 | return this.options.defaultValue;
119 | }
120 |
121 | notifyListeners() {
122 | this.listeners.forEach((listener) => listener());
123 | }
124 |
125 | setItem(value: SetStateAction) {
126 | const newValue =
127 | typeof value === "function"
128 | ? (value as (prevState: T | undefined) => T | undefined)(
129 | this.getSnapshot()
130 | )
131 | : value;
132 | this.setValue(newValue);
133 | }
134 | }
135 |
136 | function getOrCreateStore(
137 | key: string,
138 | options: Options
139 | ): StorageStore {
140 | const storeType = options.type || "local";
141 | let store = globalStores[storeType][key];
142 | if (!store) {
143 | store = globalStores[storeType][key] = new StorageStore(key, options);
144 | }
145 | return store;
146 | }
147 |
148 | type Return = [T | undefined, Dispatch>];
149 |
150 | function useStorage(key: string, options?: Options): Return {
151 | const store = getOrCreateStore(key, options || {});
152 |
153 | const state: T | undefined = useSyncExternalStore(
154 | store.subscribe.bind(store),
155 | store.getSnapshot.bind(store),
156 | store.getServerSnapshot.bind(store)
157 | );
158 |
159 | const setItem = useCallback(
160 | (value: SetStateAction) => {
161 | store.setItem(value);
162 | },
163 | [store]
164 | );
165 |
166 | return [state, setItem];
167 | }
168 |
169 | export default useStorage;
170 |
--------------------------------------------------------------------------------
/hooks/useTOCHighlights.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Facebook, Inc. and its affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | import { useEffect, useState } from "react";
9 |
10 | function useTOCHighlight(
11 | linkClassName: string,
12 | linkActiveClassName: string,
13 | topOffset: number
14 | ) {
15 | const [lastActiveLink, setLastActiveLink] = useState(undefined);
16 |
17 | useEffect(() => {
18 | let headersAnchors: any = [];
19 | let links: any = [];
20 |
21 | function setActiveLink() {
22 | function getActiveHeaderAnchor() {
23 | let index = 0;
24 | let activeHeaderAnchor = null;
25 |
26 | headersAnchors = document.getElementsByClassName("anchor");
27 | while (index < headersAnchors.length && !activeHeaderAnchor) {
28 | const headerAnchor = headersAnchors[index];
29 | const { top } = headerAnchor.getBoundingClientRect();
30 |
31 | if (top >= 0 && top <= topOffset) {
32 | activeHeaderAnchor = headerAnchor;
33 | }
34 |
35 | index += 1;
36 | }
37 |
38 | return activeHeaderAnchor;
39 | }
40 |
41 | const activeHeaderAnchor = getActiveHeaderAnchor();
42 |
43 | if (activeHeaderAnchor) {
44 | let index = 0;
45 | let itemHighlighted = false;
46 |
47 | links = document.getElementsByClassName(linkClassName);
48 | while (index < links.length && !itemHighlighted) {
49 | const link = links[index];
50 | const { href } = link;
51 | const anchorValue = decodeURIComponent(
52 | href.substring(href.indexOf("#") + 1)
53 | );
54 |
55 | if (activeHeaderAnchor.id === anchorValue) {
56 | // if (lastActiveLink) {
57 | // lastActiveLink.classList.remove(
58 | // linkActiveClassName
59 | // );
60 | // }
61 | // link.classList.add(linkActiveClassName);
62 | setLastActiveLink(link);
63 | itemHighlighted = true;
64 | }
65 |
66 | index += 1;
67 | }
68 | }
69 | }
70 |
71 | document.addEventListener("scroll", setActiveLink);
72 | document.addEventListener("resize", setActiveLink);
73 |
74 | setActiveLink();
75 |
76 | return () => {
77 | document.removeEventListener("scroll", setActiveLink);
78 | document.removeEventListener("resize", setActiveLink);
79 | };
80 | }, []);
81 | // @ts-ignore
82 | return lastActiveLink?.attributes?.["href"]?.nodeValue || "";
83 | }
84 |
85 | export default useTOCHighlight;
86 |
--------------------------------------------------------------------------------
/hooks/useTags.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useTransition } from "react";
2 |
3 | export type Tag = [number, string, string];
4 | export type Tags = Tag[];
5 |
6 | export function useTags() {
7 | // tags
8 | const [isPending, startTransition] = useTransition();
9 | const [tags, setTags] = useState([]);
10 |
11 | useEffect(() => {
12 | fetch(
13 | "/lc-rating/tags.json?t=" + (new Date().getTime() / 100000).toFixed(0)
14 | )
15 | .then((res) => res.json())
16 | .then((result: Tags) => {
17 | startTransition(() => {
18 | setTags(
19 | result.sort(function (t1, t2) {
20 | return t1[2].localeCompare(t2[2]);
21 | })
22 | );
23 | });
24 | });
25 | }, []);
26 |
27 | return { tags, isPending };
28 | }
29 |
--------------------------------------------------------------------------------
/hooks/useTheme.tsx:
--------------------------------------------------------------------------------
1 | import useStorage from "@hooks/useStorage";
2 | import React, { createContext, useContext, useEffect } from "react";
3 |
4 | enum Theme {
5 | Light = "light",
6 | Dark = "dark",
7 | }
8 |
9 | interface ThemeContextValue {
10 | theme: Theme;
11 | toggleTheme: () => void;
12 | }
13 |
14 | const ThemeContext = createContext(undefined);
15 |
16 | interface ThemeProviderProps {
17 | children: React.ReactNode;
18 | }
19 |
20 | function ThemeProvider({ children }: ThemeProviderProps) {
21 | const [theme = Theme.Light, setTheme] = useStorage("theme", {
22 | defaultValue: Theme.Light,
23 | });
24 |
25 | const toggleTheme = () => {
26 | setTheme(theme === Theme.Light ? Theme.Dark : Theme.Light);
27 | };
28 |
29 | useEffect(() => {
30 | document.documentElement.setAttribute("data-bs-theme", theme);
31 | }, [theme]);
32 |
33 | const value: ThemeContextValue = {
34 | theme,
35 | toggleTheme,
36 | };
37 |
38 | return (
39 | {children}
40 | );
41 | }
42 |
43 | function useTheme(): ThemeContextValue {
44 | const context = useContext(ThemeContext);
45 |
46 | if (!context) {
47 | throw new Error("useTheme must be used within a ThemeProvider");
48 | }
49 |
50 | return context;
51 | }
52 |
53 | export { ThemeProvider, useTheme };
54 |
--------------------------------------------------------------------------------
/hooks/useZen.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useSuspenseQuery } from "@tanstack/react-query";
4 |
5 | // Question Data Type
6 | interface ConstQuestion {
7 | cont_title: string;
8 | cont_title_slug: string;
9 | title: string;
10 | title_slug: string;
11 | question_id: string;
12 | paid_only: boolean;
13 | rating: number;
14 | _hash: number;
15 | }
16 |
17 | export function useZen() {
18 | const { data, isFetching } = useSuspenseQuery({
19 | queryKey: [],
20 | queryFn: () =>
21 | fetch("/lc-rating/zenk.json")
22 | .then((res) => res.json())
23 | .then((result: ConstQuestion[]) => {
24 | return result;
25 | }),
26 | refetchOnWindowFocus: false,
27 | });
28 |
29 | return { zen: data, isPending: isFetching };
30 | }
31 |
--------------------------------------------------------------------------------
/lc-maker/0x3f_discuss.py:
--------------------------------------------------------------------------------
1 | import re
2 | import json
3 | import argparse
4 | import sys
5 | from leetcode_api import load_headers, LeetCodeApi
6 | from tqdm import tqdm
7 | from typing import List, Dict
8 | from collections import deque
9 | from dataclasses import dataclass, asdict
10 |
11 | LEETCODE_PRE_URL = "https://leetcode.cn/problems"
12 | RATING_URL = "https://raw.githubusercontent.com/zerotrac/leetcode_problem_rating/main/data.json"
13 | DISCUSSION_URL_MAP = {
14 | "0viNMK":"sliding_window",
15 | "SqopEo":"binary_search",
16 | "9oZFK9":"monotonic_stack",
17 | "YiXPXW":"grid",
18 | "dHn9Vk":"bitwise_operations",
19 | "01LUak":"graph",
20 | "tXLS3i":"dynamic_programming",
21 | "mOr1u6":"data_structure",
22 | "IYT3ss":"math",
23 | "g6KTKL":"greedy",
24 | "K0n2gO":"trees",
25 | "SJFwQI":"string",
26 | }
27 |
28 | @dataclass
29 | class Node:
30 | title: str
31 | summary: str
32 | src: str
33 | original_src: str
34 | sort: int
35 | isLeaf: bool
36 | solution: str
37 | score: int
38 | leafChild: List["Node"]
39 | nonLeafChild: List["Node"]
40 | isPremium: bool
41 | last_update: str
42 |
43 | def get_discussion(uuid: str, lc: LeetCodeApi):
44 | res = lc.qaQuestionDetail(uuid)
45 | # we only focus on title, content
46 | return (res["qaQuestion"]["title"], res["qaQuestion"]["content"], res["qaQuestion"]["updatedAt"])
47 |
48 |
49 | def refactor_summary(summary: str):
50 | summary = summary.strip()
51 | # replace all link to html format, ![]() ->
and []() ->
52 | pattern = r'!\[([^\]]+)\]\((http[s]?:\/\/[^\)]+)\)'
53 | def replace_img(match: re.Match):
54 | alt = match.group(1)
55 | src = match.group(2)
56 | return f'
'
57 | summary = re.sub(pattern, replace_img, summary)
58 |
59 | pattern = r'\[([^\]]+)\]\((http[s]?:\/\/[^\)]+)\)'
60 | def replace_link(match: re.Match):
61 | title = match.group(1)
62 | url = match.group(2)
63 | prefix_url = "https://leetcode.cn/circle/discuss/"
64 | suffix = url.split(prefix_url)
65 | if len(suffix) > 1 and suffix[1].strip('/') in DISCUSSION_URL_MAP:
66 | suffix = suffix[1].strip('/')
67 | return f'{title}'
68 | return f'{title}'
69 | return re.sub(pattern, replace_link, summary)
70 |
71 | def extract_content(contents_queue: deque) -> List[str]:
72 | res = []
73 | if contents_queue[0].startswith("#"):
74 | res.append(contents_queue.popleft().strip())
75 | else:
76 | res.append("# 介绍")
77 | while contents_queue:
78 | if contents_queue[0] and contents_queue[0][0] == "#":
79 | break
80 | cont = contents_queue.popleft().strip()
81 | if cont == "":
82 | continue
83 | res.append(cont)
84 | return res
85 |
86 | def refactor_helper(content: List[str], rating: Dict) -> Node:
87 | node = Node("", "", "", "", 0, False, "", 0, [], [], False, "")
88 | for cont in content:
89 | if cont.startswith("#"):
90 | # 这里假设了标题是#开头,且结尾没有#,否则title会出现问题
91 | node.title = cont.split("#")[-1].strip()
92 | elif cont.startswith("- ["):
93 | markdown_match = re.match(r"-\s*\[(.*?)\]\((.*?)\)\s*(?:((.*?)))?", cont)
94 | title = markdown_match.group(1)
95 | ori_src = markdown_match.group(2)
96 | additional = markdown_match.group(3)
97 | title_id = title.split(". ")[0]
98 | if title_id.isdigit():
99 | title_id = int(title_id)
100 | score = rating[title_id] if title_id in rating else None
101 | if LEETCODE_PRE_URL in ori_src:
102 | src = ori_src.split(LEETCODE_PRE_URL)[1]
103 | else:
104 | src = None
105 | solution = None
106 | isPremium = additional != None and "会员题" in additional
107 | second_markdown_match = re.match(r"\[(.*)\]\((.*)\)", cont[cont.find(")")+2:])
108 | if second_markdown_match:
109 | solution = second_markdown_match.group(2)
110 | if LEETCODE_PRE_URL in solution:
111 | solution = solution.split(LEETCODE_PRE_URL)[1]
112 | node.leafChild.append(Node(title, "", src, ori_src, 0, True, solution, score, [], [], isPremium, ""))
113 | else:
114 | node.summary += cont + "
"
115 | node.summary = refactor_summary(node.summary)
116 | return node
117 |
118 | def depth_helper(line: str) -> int:
119 | depth = 0
120 | for char in line:
121 | if char != "#":
122 | break
123 | depth += 1
124 | return depth
125 |
126 | def refactor_discussion_rec(contents_queue: deque, rating: Dict) -> Node:
127 | contents = extract_content(contents_queue)
128 | curr_dep = depth_helper(contents[0])
129 | root = refactor_helper(contents, rating)
130 | if root.title == "关联题单" or root.title == "分类题单":
131 | return None
132 | if not content_queue or depth_helper(content_queue[0]) <= curr_dep:
133 | return root
134 | while content_queue and depth_helper(content_queue[0]) > curr_dep:
135 | child = refactor_discussion_rec(content_queue, rating)
136 | if child:
137 | root.nonLeafChild.append(child)
138 | return root
139 |
140 | def get_rating():
141 | import requests
142 | from collections import defaultdict
143 | res = requests.get(RATING_URL).json()
144 | dic = defaultdict(int)
145 | for item in res:
146 | dic[item["ID"]] = item["Rating"]
147 | return dic
148 |
149 | if __name__ == "__main__":
150 | # read from args
151 | parser = argparse.ArgumentParser()
152 | parser.add_argument("--uuid", help="uuid of discussion")
153 | parser.add_argument("--o", help="title of discussion, default it as uuid")
154 | parser.add_argument("--f", help="uuids and title of discussion from a file")
155 | args = parser.parse_args()
156 | if len(sys.argv) == 1:
157 | parser.print_help()
158 | sys.exit(1)
159 | uuid = args.uuid
160 | path = args.f
161 | output_file = args.o
162 | # initialize
163 | hds = load_headers()
164 | lc = LeetCodeApi(headers=hds)
165 | rating = get_rating()
166 | uuids_title = []
167 | if path:
168 | with open(path, "r") as f:
169 | temp = f.readlines()
170 | for line in temp:
171 | uuids_title.append(line.strip().split(" "))
172 | if uuid:
173 | if not output_file:
174 | uuids_title.append([uuid, "./" + uuid + ".ts"])
175 | else:
176 | uuids_title.append([uuid, output_file])
177 | # get and analysis discussion content according to uuid
178 | for uuid, file_path in tqdm(uuids_title):
179 | title, content, last_update = get_discussion(uuid, lc)
180 | # format last_update into yyyy-mm-dd hh:mm:ss
181 | temp_split = last_update.split("T")
182 | last_update = temp_split[0] + " " + temp_split[1].split(".")[0]
183 | content = content.replace("\r\n", "\n").strip()
184 | original_src = "https://leetcode.cn/circle/discuss/" + uuid
185 | content_queue = deque(content.split("\n"))
186 | parent = Node(title, "", "", original_src, 0, False, "", 0, [], [], False, last_update)
187 | while content_queue:
188 | node = refactor_discussion_rec(content_queue, rating)
189 | if node:
190 | parent.nonLeafChild.append(node)
191 | try:
192 | with open(file_path, "w") as f:
193 | f.write("import ProblemCategory from \"@components/ProblemCatetory\";\n\nexport default" + json.dumps(asdict(parent), indent=4, ensure_ascii=False) + " as ProblemCategory;")
194 | except:
195 | print("Error: ", uuid, file_path)
196 |
197 | # make a waiting for 1s
198 | import time
199 | time.sleep(2)
200 |
--------------------------------------------------------------------------------
/lc-maker/README.md:
--------------------------------------------------------------------------------
1 | # 灵茶山题单制作工具
2 | 1. 从力扣页面 复制Cookie 替换 hds.txt 中的 Cookie 值
3 | 2. 安装依赖:`pip install -r requirements.txt` (如抛出异常 "No module named 'pip'", 可先执行 python -m ensurepip)
4 | 3. 执行:`python main.py`
5 |
6 | ## 使用灵茶山艾府的题单生成对应网页
7 |
8 | 1. 安装依赖:`pip install -r requirements.txt` (如抛出异常 "No module named 'pip'", 可先执行 python -m ensurepip)
9 | 2. 执行:`python 0x3f_discuss.py [--uuid xxxx] [--o yourpath/yourfilename] [--f path/to/discussionlist]`
10 | - 此处xxxx为尾uuid, 例如对于讨论页面https://leetcode-cn.com/circle/discuss/123456/,此处123456是这个讨论页面的uuid
11 | - `yourpath/yourfilename`为输出文件路径, 默认输出在当前目录下
12 | - `path/to/discussionlist`为讨论列表文件路径,该文件遵守以下格式:
13 | - ```
14 | uuid1 output/path/for/uuid1
15 | uuid2 output/path/for/uuid2
16 | ...
17 | ```
18 | 3. 如果生成的ts文件不在`components/containers/List/data`中,将其拖入,并在`components/containers/List/`下创建对应的文件夹以及`index.tsx`文件, 并在`app/(lc)/list`下创建对应的文件以启用
19 | 4. 同时在`components/layouts/Navbar/index.tsx`中添加对应的导航链接
--------------------------------------------------------------------------------
/lc-maker/discussion.txt:
--------------------------------------------------------------------------------
1 | 0viNMK ../components/containers/List/data/sliding_window.ts
2 | SqopEo ../components/containers/List/data/binary_search.ts
3 | 9oZFK9 ../components/containers/List/data/monotonic_stack.ts
4 | YiXPXW ../components/containers/List/data/grid.ts
5 | dHn9Vk ../components/containers/List/data/bitwise_operations.ts
6 | 01LUak ../components/containers/List/data/graph.ts
7 | tXLS3i ../components/containers/List/data/dynamic_programming.ts
8 | mOr1u6 ../components/containers/List/data/data_structure.ts
9 | IYT3ss ../components/containers/List/data/math.ts
10 | g6KTKL ../components/containers/List/data/greedy.ts
11 | K0n2gO ../components/containers/List/data/trees.ts
12 | SJFwQI ../components/containers/List/data/string.ts
--------------------------------------------------------------------------------
/lc-maker/hds.txt:
--------------------------------------------------------------------------------
1 | Accept: */*
2 | Accept-Encoding: gzip, deflate, br, zstd
3 | Accept-Language: zh-CN,zh;q=0.9
4 | Cookie: ***
5 | Host: leetcode.cn
6 | Origin: https://leetcode.cn
7 | Referer: https://leetcode.cn/
8 | Sec-Fetch-Dest: empty
9 | Sec-Fetch-Mode: cors
10 | Sec-Fetch-Site: same-origin
11 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36
12 | content-type: application/json
13 | sec-ch-ua: "Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"
14 | sec-ch-ua-mobile: ?0
15 | sec-ch-ua-platform: "Windows"
16 |
--------------------------------------------------------------------------------
/lc-maker/js/README.md:
--------------------------------------------------------------------------------
1 | ## 功能
2 | 获取提单页面 分类列表
3 | ## 步骤
4 | - index.js 内容复制,在题单页打开浏览器控制台,粘贴执行~
5 | - (应该)可以看到控制台输出的可复制内容
--------------------------------------------------------------------------------
/lc-maker/js/index.js:
--------------------------------------------------------------------------------
1 | var rootSelector = 'div[class="e2v1tt3 css-1ayia3m-MarkdownContent"]';
2 | var ProblemListParser = /** @class */ (function () {
3 | function ProblemListParser() {
4 | this.list = {};
5 | this.g = {};
6 | }
7 | ProblemListParser.prototype.parser = function (selector) {
8 | var _a;
9 | this.root = document.querySelector(selector);
10 | if (!this.root) {
11 | return;
12 | }
13 | var lastH1;
14 | var lastH2;
15 | var el = this.root.firstElementChild;
16 | var total = 0;
17 | while (el) {
18 | var nodeName = el.nodeName;
19 | var id = el.getAttribute("id");
20 | var title = void 0;
21 | var summary = void 0;
22 | if (id) {
23 | title = el.textContent;
24 | }
25 | if (nodeName == "H2") {
26 | lastH1 = el.textContent || "";
27 | this.g[lastH1] = "";
28 | }
29 | if (nodeName == "H3") {
30 | lastH2 = el.textContent || "";
31 | this.g[lastH2] = lastH1;
32 | }
33 | if (nodeName == "P") {
34 | summary = el.innerHTML;
35 | }
36 | if (nodeName == "UL") {
37 | if (
38 | ((_a = el.previousElementSibling) === null || _a === void 0
39 | ? void 0
40 | : _a.nodeName) == "H2"
41 | ) {
42 | lastH2 = lastH1;
43 | }
44 | var childs = this.parseList(el);
45 | for (var _i = 0, childs_1 = childs; _i < childs_1.length; _i++) {
46 | var ch = childs_1[_i];
47 | var rep1 = repr0(lastH1);
48 | var rep2 = repr(lastH2);
49 | var seq = getSeq(lastH2);
50 | var title_1 = "".concat(seq).concat(rep1, " ").concat(rep2);
51 | this.list[title_1] = this.list[title_1]
52 | ? this.list[title_1].concat(ch)
53 | : [ch];
54 | }
55 | total += childs.length;
56 | }
57 | el = el.nextElementSibling;
58 | }
59 | console.log(JSON.stringify(this.list, null, 2));
60 | console.log("total: ".concat(total));
61 | };
62 | ProblemListParser.prototype.parseList = function (col) {
63 | var _a, _b;
64 | var childs = [];
65 | for (var i = 0; i < col.children.length; i++) {
66 | var el = col.children[i];
67 | if (!el) {
68 | break;
69 | }
70 | var title =
71 | ((_a = el.firstElementChild) === null || _a === void 0
72 | ? void 0
73 | : _a.textContent) || "";
74 | var src =
75 | ((_b = el.firstElementChild) === null || _b === void 0
76 | ? void 0
77 | : _b.getAttribute("href")) || "";
78 | childs = childs.concat({ title: title, src: src });
79 | }
80 | return childs;
81 | };
82 | return ProblemListParser;
83 | })();
84 | function repr0(s) {
85 | return s.replace(/[一二三四五六七八九十]+、/g, "");
86 | }
87 | function repr(s) {
88 | return s.replace(/\s+§\d+.\d+\s+/g, "");
89 | }
90 | function getSeq(s) {
91 | var a = s.match(/§\d+.\d+/g);
92 | return a ? a[0] : "";
93 | }
94 | (function () {
95 | setTimeout(function () {
96 | return new ProblemListParser().parser(rootSelector);
97 | }, 500);
98 | })();
99 |
--------------------------------------------------------------------------------
/lc-maker/js/index.ts:
--------------------------------------------------------------------------------
1 | const rootSelector = 'div[class="e2v1tt3 css-1ayia3m-MarkdownContent"]';
2 |
3 | interface CategoryItem {
4 | title: string;
5 | src?: string;
6 | children?: CategoryItem[];
7 | }
8 | class ProblemListParser {
9 | list: Record = {};
10 | g: Record = {};
11 | root: HTMLDivElement | null = null;
12 |
13 | parser(selector: string) {
14 | this.root = document.querySelector(selector);
15 | if (!this.root) {
16 | return;
17 | }
18 | let lastH1: string = "";
19 | let lastH2: string = "";
20 | let el = this.root.firstElementChild;
21 | let total = 0;
22 | while (el) {
23 | let nodeName = el.nodeName;
24 | let id = el.getAttribute("id");
25 | let title;
26 | let summary;
27 | if (id) {
28 | title = el.textContent;
29 | }
30 | if (nodeName == "H2") {
31 | lastH1 = el.textContent || "";
32 | this.g[lastH1] = "";
33 | }
34 | if (nodeName == "H3") {
35 | lastH2 = el.textContent || "";
36 | this.g[lastH2] = lastH1;
37 | }
38 | if (nodeName == "P") {
39 | summary = el.innerHTML;
40 | }
41 | if (nodeName == "UL") {
42 | if (el.previousElementSibling?.nodeName == "H2") {
43 | lastH2 = lastH1;
44 | }
45 | let childs = this.parseList(el);
46 | for (let ch of childs) {
47 | let rep1 = repr0(lastH1);
48 | let rep2 = repr(lastH2);
49 | let seq = getSeq(lastH2);
50 | let title = `${seq}${rep1} ${rep2}`;
51 | this.list[title] = this.list[title]
52 | ? this.list[title].concat(ch)
53 | : [ch];
54 | }
55 | total += childs.length;
56 | }
57 | el = el.nextElementSibling;
58 | }
59 | }
60 |
61 | parseList(col: Element) {
62 | let childs: CategoryItem[] = [];
63 | for (let i = 0; i < col.children.length; i++) {
64 | let el = col.children[i];
65 | if (!el) {
66 | break;
67 | }
68 | let title = el.firstElementChild?.textContent || "";
69 | let src = el.firstElementChild?.getAttribute("href") || "";
70 | childs = childs.concat({ title, src });
71 | }
72 | return childs;
73 | }
74 | }
75 | function repr0(s: string) {
76 | return s.replace(/[一二三四五六七八九十]+、/g, "");
77 | }
78 |
79 | function repr(s: string) {
80 | return s.replace(/\s+§\d+.\d+\s+/g, "");
81 | }
82 |
83 | function getSeq(s: string) {
84 | let a = s.match(/§\d+.\d+/g);
85 | return a ? a[0] : "";
86 | }
87 |
88 | (function () {
89 | setTimeout(() => new ProblemListParser().parser(rootSelector), 500);
90 | })();
91 |
--------------------------------------------------------------------------------
/lc-maker/leetcode_api.py:
--------------------------------------------------------------------------------
1 | from typing import List, Dict, Any, Optional
2 | from gql import Client, gql
3 | from gql.transport.aiohttp import AIOHTTPTransport
4 | from requests import delete
5 |
6 | def load_headers():
7 | hds = {}
8 | with open("hds.txt", 'r', encoding="utf-8") as r:
9 | for line in r.readlines():
10 | sep = line.find(":")
11 | if sep != -1:
12 | hds[line[:sep]] = line[sep+1:].strip()
13 | return hds
14 |
15 | class LeetCodeApi:
16 | def __init__(self, headers) -> None:
17 | self.baseURL = "https://leetcode.cn"
18 | self.headers = headers
19 | # Select your transport with a defined url endpoint
20 | transport = AIOHTTPTransport(url="https://leetcode.cn/graphql/", headers=headers)
21 | # Create a GraphQL client using the defined transport
22 | self.client = Client(transport=transport, fetch_schema_from_transport=False)
23 |
24 | def problemList(self, skip, limit):
25 | query = gql("""
26 | query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $filters: QuestionListFilterInput) {
27 | problemsetQuestionList(
28 | categorySlug: $categorySlug
29 | limit: $limit
30 | skip: $skip
31 | filters: $filters
32 | ) {
33 | hasMore
34 | total
35 | questions {
36 | acRate
37 | difficulty
38 | freqBar
39 | frontendQuestionId
40 | isFavor
41 | paidOnly
42 | solutionNum
43 | status
44 | title
45 | titleCn
46 | titleSlug
47 | topicTags {
48 | name
49 | nameTranslated
50 | id
51 | slug
52 | }
53 | }
54 | }
55 | }
56 | """)
57 | return self.client.execute(query,variable_values={"categorySlug": "all-code-essentials", "limit": "%d" % limit, "skip": "%d" % skip}, operation_name="problemsetQuestionList")
58 |
59 | def getMyFav(self) -> Dict[str, Any]:
60 | query = gql("\n query favoriteMyFavorites($limit: Int, $skip: Int) {\n __typename\n favoriteMyFavorites(limit: $limit, skip: $skip) {\n hasMore\n total\n favorites {\n acNumber\n coverUrl\n created\n isPublicFavorite\n name\n link\n idHash\n questionNumber\n creator {\n realName\n userSlug\n __typename\n }\n __typename\n }\n __typename\n }\n }\n ")
61 | variables = {"limit": "100", "skip": "0"}
62 | op = "favoriteMyFavorites"
63 | return self.client.execute(query, variable_values=variables, operation_name=op)
64 |
65 | def getMyFav_0(self):
66 | query = gql("\n query myFavoriteList {\n myCreatedFavoriteList {\n favorites {\n coverUrl\n coverEmoji\n coverBackgroundColor\n hasCurrentQuestion\n isPublicFavorite\n lastQuestionAddedAt\n name\n slug\n }\n hasMore\n totalLength\n }\n myCollectedFavoriteList {\n hasMore\n totalLength\n favorites {\n coverUrl\n coverEmoji\n coverBackgroundColor\n hasCurrentQuestion\n isPublicFavorite\n name\n slug\n lastQuestionAddedAt\n }\n }\n}\n ")
67 | return self.client.execute(query, variable_values={}, operation_name="myFavoriteList")
68 |
69 | def addFav(self, name: str, questionId: Optional[str],isPublic: Optional[bool] = True):
70 | operationName = "addQuestionToNewFavorite"
71 | variables = {
72 | "questionId": questionId,
73 | "isPublicFavorite": isPublic,
74 | "name": name
75 | }
76 | query = gql("\n mutation addQuestionToNewFavorite(\n $name: String!\n $isPublicFavorite: Boolean!\n $questionId: String!\n ) {\n addQuestionToNewFavorite(\n name: $name\n isPublicFavorite: $isPublicFavorite\n questionId: $questionId\n ) {\n ok\n error\n name\n isPublicFavorite\n favoriteIdHash\n questionId\n __typename\n }\n }\n ")
77 | return self.client.execute(query, variable_values=variables, operation_name=operationName)
78 |
79 | def delFav(self, idHash):
80 | return delete((self.baseURL + "/list/api/%s") % idHash, headers=self.headers)
81 |
82 | def addQuestionToFav(self, questionId, favoriteIdHash):
83 | query = gql("""
84 | mutation addQuestionToFavorite($favoriteIdHash: String!, $questionId: String!) {
85 | addQuestionToFavorite(favoriteIdHash: $favoriteIdHash, questionId: $questionId) {
86 | ok
87 | error
88 | favoriteIdHash
89 | questionId
90 | }
91 | }
92 | """)
93 | return self.client.execute(query, variable_values={"favoriteIdHash": favoriteIdHash, "questionId": questionId}, operation_name="addQuestionToFavorite")
94 |
95 | def delQuestionFromFav(self, questionId, favoriteIdHash):
96 | query = gql("""
97 | mutation removeQuestionFromFavorite($favoriteIdHash: String!, $questionId: String!) {
98 | removeQuestionFromFavorite(
99 | favoriteIdHash: $favoriteIdHash
100 | questionId: $questionId
101 | ) {
102 | ok
103 | error
104 | favoriteIdHash
105 | questionId
106 | }
107 | }
108 | """)
109 | return self.client.execute(query, variable_values={"favoriteIdHash": favoriteIdHash, "questionId": questionId}, operation_name="removeQuestionFromFavorite")
110 |
111 | def queryByTitleSlug(self, titleSlug):
112 | query = gql("""
113 | query questionTitle($titleSlug: String!) {
114 | question(titleSlug: $titleSlug) {
115 | questionId
116 | questionFrontendId
117 | title
118 | titleSlug
119 | isPaidOnly
120 | difficulty
121 | likes
122 | dislikes
123 | categoryTitle
124 | }
125 | }
126 | """)
127 | return self.client.execute(query, variable_values={"titleSlug": titleSlug}, operation_name="questionTitle")
128 |
129 | def qaQuestionDetail(self, uuid):
130 | query = gql("""query qaQuestionDetail($uuid: ID!) {\n qaQuestion(uuid: $uuid) {\n ...qaQuestion\n myAnswerId\n __typename\n }\n}\n\nfragment qaQuestion on QAQuestionNode {\n ipRegion\n uuid\n slug\n title\n thumbnail\n summary\n content\n slateValue\n sunk\n pinned\n pinnedGlobally\n byLeetcode\n isRecommended\n isRecommendedGlobally\n subscribed\n hitCount\n numAnswers\n numPeopleInvolved\n numSubscribed\n createdAt\n updatedAt\n status\n identifier\n resourceType\n articleType\n alwaysShow\n alwaysExpand\n score\n favoriteCount\n isMyFavorite\n isAnonymous\n canEdit\n reactionType\n atQuestionTitleSlug\n blockComments\n reactionsV2 {\n count\n reactionType\n __typename\n }\n tags {\n name\n nameTranslated\n slug\n imgUrl\n tagType\n __typename\n }\n subject {\n slug\n title\n __typename\n }\n contentAuthor {\n ...contentAuthor\n __typename\n }\n realAuthor {\n ...realAuthor\n __typename\n }\n __typename\n}\n\nfragment contentAuthor on ArticleAuthor {\n username\n userSlug\n realName\n avatar\n __typename\n}\n\nfragment realAuthor on UserNode {\n username\n profile {\n userSlug\n realName\n userAvatar\n __typename\n }\n __typename\n}\n""")
131 | return self.client.execute(query, variable_values={"uuid": uuid}, operation_name="qaQuestionDetail")
132 |
--------------------------------------------------------------------------------
/lc-maker/main.py:
--------------------------------------------------------------------------------
1 | import time
2 | from leetcode_api import LeetCodeApi
3 |
4 | def load_headers():
5 | hds = {}
6 | with open("hds.txt", 'r', encoding="utf-8") as r:
7 | for line in r.readlines():
8 | sep = line.find(":")
9 | if sep != -1:
10 | hds[line[:sep]] = line[sep+1:].strip()
11 | return hds
12 |
13 | def load_list_as_dict():
14 | with open("list.json", 'r', encoding="utf-8") as r:
15 | import json
16 | return json.load(r)
17 |
18 | def deleteAllFavs(lc: LeetCodeApi):
19 | # 删除个人所有收藏夹
20 | for fav in lc.getMyFav()["favoriteMyFavorites"]["favorites"]:
21 | print(lc.delFav(fav["idHash"]))
22 |
23 | '''
24 | 创建题单 需要先提供 list.json
25 | '''
26 | def createList(lc: LeetCodeApi):
27 | pb = load_list_as_dict()
28 | cnt = 0
29 | for k, v in pb.items():
30 | hash = ""
31 | for i, q in enumerate(v):
32 | slug = q["src"][29:-1]
33 | cnt += 1
34 | try:
35 | id = lc.queryByTitleSlug(slug)["question"]["questionId"]
36 | if i == 0:
37 | hash = lc.addFav(k, id, True)["addQuestionToNewFavorite"]["favoriteIdHash"]
38 | else:
39 | print(lc.addQuestionToFav(id, hash))
40 | except:
41 | print("error: ", slug)
42 | pass
43 | if cnt % 50 == 0: # 防机器人识别
44 | time.sleep(5)
45 |
46 |
47 | '''
48 | 打印我的题单列表 (markdown 格式)
49 | '''
50 | def printList():
51 | links = [ (fav['name'], 'https://leetcode.cn/problem-list/%s' % fav['idHash']) for fav in lc.getMyFav()["favoriteMyFavorites"]["favorites"] ]
52 | print("\n".join( '[%s](%s)' % (title, link) for title, link in links ))
53 |
54 | if __name__ == "__main__":
55 | hds = load_headers()
56 | lc = LeetCodeApi(headers=hds)
57 | # createList(lc)
58 | printList()
59 |
--------------------------------------------------------------------------------
/lc-maker/rating-list.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, Any
2 | from requests import get
3 | from collections import defaultdict
4 | import time
5 | from leetcode_api import load_headers, LeetCodeApi
6 |
7 | rating_data_url = 'https://raw.githubusercontent.com/zerotrac/leetcode_problem_rating/main/data.json'
8 |
9 | def group_mapping() -> Dict[str, Any]:
10 | data = get(rating_data_url)
11 | if data.status_code != 200:
12 | return {}
13 | g = defaultdict(list)
14 | for v in data.json():
15 | r = v['Rating']
16 | if r < 1200:
17 | g['<1200'].append(v)
18 | elif r < 1400:
19 | g['[1200, 1400)'].append(v)
20 | elif r < 1600:
21 | g['[1400, 1600)'].append(v)
22 | elif r < 1900:
23 | g['[1600, 1900)'].append(v)
24 | elif r < 2100:
25 | g['[1900, 2100)'].append(v)
26 | elif r < 2400:
27 | g['[2100, 2400)'].append(v)
28 | else:
29 | g['>=2400'].append(v)
30 | for k in g.keys():
31 | g[k].sort(key=lambda v: v['Rating'])
32 | return g
33 |
34 | if __name__ == '__main__':
35 | hds = load_headers()
36 | lc = LeetCodeApi(headers=hds)
37 | g = group_mapping()
38 | cnt = 0
39 | for k, plist in g.items():
40 | hash = ""
41 | for i, p in enumerate(plist):
42 | slug = p['TitleSlug']
43 | cnt += 1
44 | try:
45 | id = lc.queryByTitleSlug(slug)["question"]["questionId"]
46 | if i == 0:
47 | hash = lc.addFav("# " + k, id, True)["addQuestionToNewFavorite"]["favoriteIdHash"]
48 | else:
49 | print(lc.addQuestionToFav(id, hash))
50 | except:
51 | print("error: ", slug)
52 | pass
53 | if cnt % 50 == 0: # 防机器人识别
54 | time.sleep(5)
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/lc-maker/requirements.txt:
--------------------------------------------------------------------------------
1 | aiohttp==3.9.5
2 | aiosignal==1.3.1
3 | anyio==4.3.0
4 | async-timeout==4.0.3
5 | attrs==23.2.0
6 | backoff==2.2.1
7 | brotlipy==0.7.0
8 | certifi==2024.2.2
9 | cffi==1.17.1
10 | charset-normalizer==3.3.2
11 | exceptiongroup==1.2.2
12 | frozenlist==1.4.1
13 | gql==3.5.0
14 | graphql-core==3.2.3
15 | idna==3.7
16 | multidict==6.0.5
17 | pycparser==2.22
18 | requests==2.32.2
19 | sniffio==1.3.1
20 | tqdm==4.66.6
21 | typing_extensions==4.12.2
22 | urllib3==2.2.1
23 | yarl==1.9.4
24 |
--------------------------------------------------------------------------------
/lc-maker/socre_fillter/main.py:
--------------------------------------------------------------------------------
1 | from json import load, dump
2 | from typing import Dict, Any
3 | from pymongo import MongoClient, collection, database
4 | from math import ceil
5 |
6 | def get_database():
7 |
8 | # 提供 mongodb atlas url 以使用 pymongo 将 python 连接到 mongodb
9 | CONNECTION_STRING = "mongodb://root:o039fjf1Ef@127.0.0.1:27017"
10 |
11 | # 使用 MongoClient 创建连接。您可以导入 MongoClient 或者使用 pymongo.MongoClient
12 | client = MongoClient(CONNECTION_STRING)
13 |
14 | # 为我们的示例创建数据库(我们将在整个教程中使用相同的数据库
15 | return client["lc"]
16 |
17 | class ScoreGetter:
18 | lc: database.Database
19 | def __init__(self):
20 | self.lc = get_database()
21 |
22 | def parse(self):
23 | with open('list.json', 'r', encoding="utf-8") as r:
24 | d = load(r)
25 | self.deep_into(d)
26 | with open("list_v1.json", "w", encoding="utf-8") as w:
27 | dump(d, w, ensure_ascii=False)
28 |
29 | def get_score_by_slug(self, slug: str) -> Any:
30 | item = self.lc['problem'].find_one({"titleSlug": slug})
31 | if item: return item['rating']
32 |
33 | def deep_into(self, d: Dict[str, Any]):
34 | if "child" in d:
35 | for v in d["child"]:
36 | self.deep_into(v)
37 | if "src" in d and "child" not in d:
38 | # query score
39 | slug = d["src"].strip("/")
40 | score = self.get_score_by_slug(slug)
41 | print(slug, score)
42 | if score:
43 | d['score'] = ceil(score)
44 |
45 | if __name__ == "__main__":
46 | # with open('math.json') as r:
47 | # d = load(r)
48 | # deep_into(d)
49 | sg = ScoreGetter()
50 | sg.parse()
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/mdx-components.tsx:
--------------------------------------------------------------------------------
1 | import SyntaxHighlighter from "react-syntax-highlighter";
2 | import type { MDXComponents } from "mdx/types";
3 |
4 | function code({
5 | className = "",
6 | children,
7 | ...properties
8 | }: {
9 | className?: string;
10 | [key: string]: any;
11 | }) {
12 | const match = /language-(\w+)/.exec(className || "");
13 | return match ? (
14 |
15 |
21 |
22 | ) : (
23 |
24 | );
25 | }
26 |
27 | export function useMDXComponents(components: MDXComponents): MDXComponents {
28 | return {
29 | ...components,
30 | code,
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | import remarkFrontmatter from "remark-frontmatter";
2 | import remarkMdxFrontmatter from "remark-mdx-frontmatter";
3 | import rehypeKatex from "rehype-katex";
4 | import remarkMath from "remark-math";
5 | import rehypePrettyCode from "rehype-pretty-code";
6 | import createMDX from "@next/mdx";
7 |
8 | /** @type {import('rehype-pretty-code').Options} */
9 | const options = {
10 | keepBackground: true,
11 | // See Options section below.
12 | theme: "one-dark-pro",
13 | };
14 |
15 | /**
16 | * @type {import('next').NextConfig}
17 | */
18 | const nextConfig = {
19 | output: "export",
20 | basePath: "/lc-rating",
21 | distDir: "build",
22 | };
23 |
24 | const withMDX = createMDX({
25 | // Add markdown plugins here, as desired
26 | options: {
27 | jsx: true,
28 | rehypePlugins: [rehypeKatex, [rehypePrettyCode, options]],
29 | remarkPlugins: [
30 | [remarkMdxFrontmatter, { name: "matter" }],
31 | remarkMdxFrontmatter,
32 | remarkMath,
33 | ],
34 | },
35 | });
36 |
37 | export default withMDX(nextConfig);
38 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lc-rating",
3 | "private": false,
4 | "homepage": "https://huxulm.github.io/lc-rating",
5 | "version": "0.0.4",
6 | "scripts": {
7 | "dev": "next dev",
8 | "build": "next build",
9 | "start": "next start",
10 | "lint": "next lint"
11 | },
12 | "dependencies": {
13 | "@mdx-js/loader": "^3.0.1",
14 | "@mdx-js/react": "^3.0.1",
15 | "@next/mdx": "^14.2.2",
16 | "@tanstack/match-sorter-utils": "^8.8.4",
17 | "@tanstack/react-query": "^5.45.1",
18 | "@tanstack/react-table": "^8.15.3",
19 | "axios": "1.7.7",
20 | "bootstrap": "^5.3.0",
21 | "clsx": "^2.1.1",
22 | "github-star-badge": "^1.1.6",
23 | "next": "^14.1.4",
24 | "react": "^18.2.0",
25 | "react-bootstrap": "^2.7.4",
26 | "react-dom": "^18.2.0",
27 | "react-draggable": "^4.4.6",
28 | "react-icons": "^5.2.1",
29 | "react-query": "^3.39.3",
30 | "react-router-dom": "^6.11.2",
31 | "react-syntax-highlighter": "^15.5.0",
32 | "rehype-katex": "^7.0.0",
33 | "rehype-pretty-code": "^0.13.1",
34 | "remark-frontmatter": "^5.0.0",
35 | "remark-gfm": "^4.0.0",
36 | "remark-math": "^6.0.0",
37 | "remark-mdx-frontmatter": "^4.0.0",
38 | "shiki": "^1.3.0",
39 | "web-vitals": "^3.3.1"
40 | },
41 | "devDependencies": {
42 | "@types/mdx": "^2.0.13",
43 | "@types/node": "20.12.7",
44 | "@types/react": "^18.0.28",
45 | "@types/react-dom": "^18.0.11",
46 | "@types/react-syntax-highlighter": "^15.5.13",
47 | "eslint": "8.57.1",
48 | "eslint-config-next": "15.2.1",
49 | "sass": "^1.62.1",
50 | "typescript": "^5.0.2"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/public/favico.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/tags.json:
--------------------------------------------------------------------------------
1 | [[28514800,"Radix Sort","基数排序"],[37026620,"Combinatorics","组合数学"],[42980886,"Simulation","模拟"],[156137976,"Union Find","并查集"],[300022785,"Shell","Shell"],[326262884,"Counting","计数"],[326993225,"Segment Tree","线段树"],[365099244,"Matrix","矩阵"],[398550328,"String","字符串"],[464032360,"Bitmask","状态压缩"],[477549556,"Eulerian Circuit","欧拉回路"],[587523799,"Brainteaser","脑筋急转弯"],[601298120,"Data Stream","数据流"],[631064497,"Biconnected Component","双连通分量"],[711820689,"Geometry","几何"],[1110971868,"Reservoir Sampling","水塘抽样"],[1157090682,"Line Sweep","扫描线"],[1194511624,"Randomized","随机化"],[1217109157,"Shortest Path","最短路"],[1271117903,"Iterator","迭代器"],[1288014335,"Binary Tree","二叉树"],[1388774735,"Sorting","排序"],[1431509416,"Suffix Array","后缀数组"],[1438644433,"Strongly Connected Component","强连通分量"],[1463482908,"Enumeration","枚举"],[1503330480,"String Matching","字符串匹配"],[1562005820,"Hash Function","哈希函数"],[1649501183,"Stack","栈"],[1713688490,"Dynamic Programming","动态规划"],[1736422075,"Minimum Spanning Tree","最小生成树"],[1837839573,"Tree","树"],[1855906840,"Concurrency","多线程"],[1969930655,"Prefix Sum","前缀和"],[1988863599,"Topological Sort","拓扑排序"],[1991642727,"Recursion","递归"],[1992185659,"Binary Search","二分查找"],[2106869857,"Trie","字典树"],[2115531193,"Rolling Hash","滚动哈希"],[2130700395,"Interactive","交互"],[2201204921,"Greedy","贪心"],[2242663311,"Backtracking","回溯"],[2302066520,"Divide and Conquer","分治"],[2321067302,"Array","数组"],[2364557342,"Binary Search Tree","二叉搜索树"],[2412173278,"Bucket Sort","桶排序"],[2425219275,"Binary Indexed Tree","树状数组"],[2476338297,"Monotonic Queue","单调队列"],[2494469528,"Depth-First Search","深度优先搜索"],[2705407897,"Memoization","记忆化搜索"],[2707807672,"Database","数据库"],[2793020174,"Merge Sort","归并排序"],[2822699490,"Bit Manipulation","位运算"],[2879114835,"Counting Sort","计数排序"],[2896989895,"Ordered Set","有序集合"],[2960422376,"Heap (Priority Queue)","堆(优先队列)"],[3027943496,"Hash Table","哈希表"],[3031138664,"Sliding Window","滑动窗口"],[3111650887,"Graph","图"],[3222202221,"Doubly-Linked List","双向链表"],[3304615908,"Quickselect","快速选择"],[3656873726,"Monotonic Stack","单调栈"],[3682322655,"Linked List","链表"],[3837742759,"Game Theory","博弈"],[3861735881,"Breadth-First Search","广度优先搜索"],[3986329634,"Number Theory","数论"],[4001929615,"Math","数学"],[4049367376,"Probability and Statistics","概率与统计"],[4108302520,"Queue","队列"],[4115727122,"Two Pointers","双指针"],[4214343119,"Design","设计"],[4291276582,"Rejection Sampling","拒绝采样"]]
2 |
--------------------------------------------------------------------------------
/screenshot0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huxulm/lc-rating/08a6e09889a521d79d50657bfd88bd279d0428fb/screenshot0.png
--------------------------------------------------------------------------------
/screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huxulm/lc-rating/08a6e09889a521d79d50657bfd88bd279d0428fb/screenshot1.png
--------------------------------------------------------------------------------
/scss/_bs.scss:
--------------------------------------------------------------------------------
1 | // variable overrides
2 | $primary: rgb(160, 201, 255);
3 | $link-decoration: none;
4 | $color-mode-type: data;
5 | $link-color-dark: rgb(84, 107, 255);
6 | $link-color: rgb(0, 0, 0);
7 |
8 | @import "bootstrap/scss/bootstrap";
9 |
10 | // customize
11 | .btn-secondary {
12 | --bs-btn-active-bg: #e0e1e1;
13 | }
14 | .btn-primary {
15 | --bs-btn-border-color: transparent;
16 | --bs-btn-hover-border-color: white;
17 | }
--------------------------------------------------------------------------------
/scss/_common.scss:
--------------------------------------------------------------------------------
1 | @import "./bs";
2 | body {
3 | margin: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
13 | monospace;
14 | }
15 |
16 | a:hover {
17 | cursor: pointer;
18 | }
19 |
20 | :root {
21 | --bs-link-decoration: none;
22 | }
23 |
24 | .contest {
25 | padding: 0 3.5rem;
26 | width: fit-content;
27 | }
28 |
29 | .col-contest {
30 | display: flex;
31 | flex-direction: row;
32 | justify-content: space-around;
33 | }
34 |
35 | .row-selected {
36 | background: rgba(156, 238, 161, 1);
37 | font-weight: 900;
38 | }
39 |
40 | :root {
41 | --bs-link-decoration: none;
42 | }
43 |
44 | .contest-table {
45 | padding: 0;
46 | }
47 |
48 | .cursor-pointer {
49 | cursor: pointer;
50 | text-align: center;
51 | }
52 |
53 | .right-side {
54 | position: fixed;
55 | left: calc(100vw - .9rem);
56 | background: rgba(0, 0, 0, 0.01);
57 | width: 100vh;
58 | height: 2.5rem;
59 | top: 0;
60 | z-index: 1000;
61 | transform: rotate(90deg);
62 | transform-origin: top left;
63 | }
64 |
65 | .select-none {
66 | user-select: none;
67 | }
68 |
69 | @include media-breakpoint-down(sm) {
70 | .right-side {
71 | position: inherit;
72 | background: rgba(0, 0, 0, 0.01);
73 | width: 100vw;
74 | height: 2.5rem;
75 | transform: rotate(0);
76 | transform-origin: top left;
77 | }
78 | }
79 | // navbar style
80 | #nav-cl {
81 | color: #8ef2e9;
82 | }
83 | #nav-tr {
84 | color: #513ff7;
85 | }
86 | #nav-0x3f {
87 | color: #a41eae;
88 | }
89 | #nav-pg {
90 | color: #ffa246;
91 | }
92 | #nav-pl {
93 | color:#EA3323;
94 | }
95 |
96 | #nav-cl,#nav-tr,#nav-0x3f,#nav-pg,#nav-pl {
97 | font-weight: 700;
98 | background: white;
99 | }
100 |
101 | // resizer style
102 | .resizer {
103 | position: absolute;
104 | right: 0;
105 | top: 0;
106 | height: 100%;
107 | width: 5px;
108 | background: rgba(0, 0, 0, 0.5);
109 | cursor: col-resize;
110 | user-select: none;
111 | touch-action: none;
112 | }
113 |
114 | .resizer.isResizing {
115 | background: blue;
116 | opacity: 1;
117 | }
118 |
119 | @media (hover: hover) {
120 | .resizer {
121 | opacity: 0;
122 | }
123 |
124 | *:hover>.resizer {
125 | opacity: 1;
126 | }
127 | }
128 |
129 | .contest {
130 | display: flex;
131 | flex-direction: row;
132 | justify-content: space-around;
133 | }
134 | .row-selected {
135 | background: rgba(156, 238, 161, 0.5);
136 | font-weight: 900;
137 | }
138 |
139 | .right-side::after {
140 | right: 0;
141 | }
142 |
143 | .th-center {
144 | display: flex;
145 | justify-content: center;
146 | flex-direction: row;
147 | }
148 |
149 | .backtop {
150 | border-radius: 50%;
151 | position: fixed;
152 | bottom: 20px;
153 | right: 5px;
154 | width: 1.5rem;
155 | height: 1.5rem;
156 | text-align: center;
157 | display: block;
158 | }
159 |
160 | table>thead {
161 | z-index: 10;
162 | }
163 |
164 | table,
165 | .divTable {
166 | border: 1px solid lightgray;
167 | width: fit-content;
168 | table-layout: fixed;
169 | display: table;
170 | }
171 |
172 | .tr {
173 | display: flex;
174 | }
175 |
176 | tr,
177 | .tr {
178 | width: fit-content;
179 | height: 30px;
180 | }
181 |
182 | th,
183 | .th,
184 | td,
185 | .td {
186 | box-shadow: inset 0 0 0 1px lightgray;
187 | padding: 0.2rem;
188 | overflow: hidden;
189 | }
190 |
191 | th,
192 | .th {
193 | padding: 2px 4px;
194 | position: relative;
195 | font-weight: bold;
196 | text-align: center;
197 | vertical-align: middle;
198 | }
199 |
200 | td,
201 | .td {
202 | height: 30px;
203 | }
204 |
205 | .tb-overflow {
206 | overflow: hidden;
207 | white-space: nowrap;
208 | text-overflow: ellipsis;
209 | position: relative;
210 |
211 | .fr-wrapper {
212 | top: .5rem;
213 | right: 0.5rem;
214 | position: absolute;
215 | }
216 |
217 | .fr {
218 | position: relative;
219 | right: 0;
220 | color: rgb(49, 200, 49);
221 | font-weight: 900;
222 | font-family: 'Courier New', Courier, monospace;
223 | }
224 | }
225 |
226 |
227 | .ans {
228 | color: black!important;
229 | font-size: 1.5rem!important;
230 | font-weight: normal;
231 | text-decoration: none;
232 | &:hover {
233 | transform: scale(1.2);
234 | }
235 | }
236 |
237 | .link {
238 | text-decoration: underline!important;
239 | text-underline-offset: 3px;
240 | }
241 |
242 | // scrollbar
243 | /*
244 | * STYLE 2
245 | */
246 | ::-webkit-scrollbar-track {
247 | -webkit-box-shadow: inset 0 0 6px rgba(143, 143, 143, 0.3);
248 | border-radius: 4px;
249 | background-color: #eaeaea;
250 | }
251 |
252 | ::-webkit-scrollbar {
253 | width: 6px;
254 | background-color: transparent;
255 | }
256 | // horizontal scrollbar width set to 6px
257 | ::-webkit-scrollbar:horizontal {
258 | height: 8px;
259 | }
260 |
261 | ::-webkit-scrollbar-thumb {
262 | border-radius: 6px;
263 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
264 | background-color: #a6a6a6;
265 | }
266 |
267 | ::-webkit-scrollbar-track {
268 | -webkit-box-shadow: inset 0 0 6px rgba(143, 143, 143, 0.3);
269 | border-radius: 4px;
270 | background-color: #eaeaea;
271 | }
272 |
273 | [data-bs-theme] {
274 | --rating-color-0: #848484;
275 | --rating-color-1: #595959;
276 | --rating-color-2: #4BA59E;
277 | --rating-color-3: #1B01F5;
278 | --rating-color-4: #9B1EA4;
279 | --rating-color-5: #F09235;
280 | --rating-color-6: #EA3323;
281 | --rating-color-7: #EA3323;
282 | }
283 |
284 | [data-bs-theme=dark] {
285 | color-scheme: dark;
286 | --rating-color-0: #c5c4c4;
287 | --rating-color-1: #ffffff;
288 | --rating-color-2: #74fff3;
289 | --rating-color-3: #6857ff;
290 | --rating-color-4: #ec60f6;
291 | --rating-color-5: #f8a95b;
292 | --rating-color-6: #f35041;
293 | --rating-color-7: #f35041;
294 | }
295 |
296 | .topcoder-like-circle {
297 | cursor: pointer;
298 | display: inline-block;
299 | border-style: solid;
300 | border-width: 1px;
301 | }
302 |
303 | .rating-circle {
304 | display: inline-block;
305 | position: relative;
306 | height: 16px;
307 | width: 16px;
308 | margin-right: 4px;
309 | margin-top: 2px;
310 | background: transparent;
311 | }
312 | .inner-circle {
313 | display: block;
314 | border-radius: 50%;
315 | border-style: solid;
316 | border-width: 1.5px;
317 | width: 100%;
318 | height: 100%;
319 | }
320 | .inner-circle[data="top"] {
321 | border-color: var(--rating-color-6)!important;
322 | }
323 | .inner-circle__plus {
324 | position: absolute;
325 | display: block;
326 | width: 6px;
327 | height: 6px;
328 | border-radius: 50%;
329 | background-color: red;
330 | top: 5px;
331 | left: 5px;
332 | }
333 | .ff-st {
334 | font-family: 'SimSun', 'STSong', '宋体', 'sans-serif';
335 | }
336 | .ff-ht {
337 | font-family: '黑体', '微软雅黑';
338 | }
339 | .rating-color-0,.rating-color-1,.rating-color-2,.rating-color-3,.rating-color-4,.rating-color-5,.rating-color-6 {
340 | font-weight: bold;
341 | }
342 |
343 | .rating-color-0 {
344 | color: var(--rating-color-0)
345 | }
346 | .rating-color-1 {
347 | color: var(--rating-color-1)
348 | }
349 | .rating-color-2 {
350 | color: var(--rating-color-2)
351 | }
352 | .rating-color-3 {
353 | color: var(--rating-color-3)
354 | }
355 | .rating-color-4 {
356 | color: var(--rating-color-4)
357 | }
358 | .rating-color-5 {
359 | color: var(--rating-color-5)
360 | }
361 | .rating-color-6 {
362 | color: var(--rating-color-6)
363 | }
364 | .rating-color-7 {
365 | color: var(--rating-color-7);
366 | font-weight: 900;
367 | }
368 |
--------------------------------------------------------------------------------
/scss/_gh.scss:
--------------------------------------------------------------------------------
1 |
2 | *{
3 | margin: 0;
4 | text-decoration: none;
5 | }
6 |
7 | .github-star-badge {
8 | display: inline-flex;
9 | padding-top: 0rem;
10 | padding-bottom: 0rem;
11 | padding-left: 0.7rem;
12 | padding-right: 0.7rem;
13 | gap: 0.2rem;
14 | align-items: center;
15 | border-radius: 0.375rem;
16 | max-width: fit-content;
17 | background-color: #fff;
18 | color: #111;
19 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
20 | transition: all 0.3s ease;
21 | font-family: "Inter", sans-serif;
22 | }
23 |
24 | @media (prefers-color-scheme: dark) {
25 |
26 | .github-star-badge {
27 | background-color: #111;
28 | color: #fff;
29 | }
30 |
31 | .github-star-badge.light {
32 | background-color: #fff;
33 | color: #111;
34 | }
35 |
36 | .github-star-badge.basic img {
37 | filter: invert(1);
38 | }
39 |
40 | .github-star-badge.basic.light img {
41 | filter: none;
42 | }
43 | }
44 |
45 | .github-star-badge.dark {
46 | background-color: #111;
47 | color: white;
48 | }
49 |
50 | .github-star-badge.light {
51 | background-color: #fff;
52 | color: #111;
53 | }
54 |
55 | .github-star-badge:hover {
56 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.1), 0 2px 5px 0 rgba(0, 0, 0, 0.06);
57 | }
58 |
59 | .github-star-badge.dark:hover {
60 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.1), 0 2px 5px 0 rgba(0, 0, 0, 0.06);
61 | }
62 |
63 | .github-star-badge.basic {
64 | padding-top: 0.4rem;
65 | padding-bottom: 0.4rem;
66 | padding-left: 0.5rem;
67 | padding-right: 0.6rem;
68 | gap: 0rem;
69 | max-width: fit-content;
70 | border-radius: 0.275rem;
71 | }
72 |
73 | .github-star-badge.basic svg {
74 | margin-left: 0.2rem;
75 | margin-right: 0.2rem;
76 | color: #6B7280;
77 | transform: translateY(-0.05rem);
78 | }
79 |
80 | .github-star-badge.basic.dark img {
81 | filter: invert(1);
82 | }
83 |
84 | .github-star-badge.basic:hover svg {
85 | color: #f6e05e;
86 | }
87 |
--------------------------------------------------------------------------------
/scss/_list.scss:
--------------------------------------------------------------------------------
1 | .problem-list {
2 | display: grid;
3 | grid-template-areas: "toc content";
4 | grid-template-rows: 1fr;
5 | grid-template-columns: 1fr 4fr;
6 | z-index: -1;
7 | }
8 |
9 | .pb-content {
10 | display: grid;
11 | grid-area: content;
12 | font-family: "宋体", "SimSun", "STSong", "sans-serif";
13 | }
14 |
15 | @include media-breakpoint-down(md) {
16 | .pb-content {
17 | margin: 0 !important;
18 | padding: 0 !important;
19 | }
20 | .problem-list {
21 | display: block;
22 | }
23 | .toc {
24 | display: none;
25 | }
26 | }
27 |
28 | @keyframes blink {
29 | 50% {
30 | background-color: yellow;
31 | }
32 | }
33 |
34 | .blinking-effect {
35 | animation: blink 1s ease-in-out infinite;
36 | }
37 |
--------------------------------------------------------------------------------
/scss/_search.scss:
--------------------------------------------------------------------------------
1 | .search {
2 | margin: 1rem auto;
3 | padding: 0 2rem;
4 |
5 | li[class='search-input'] {
6 | position: sticky;
7 | top: 5rem;
8 | background: white;
9 | margin-bottom: 1rem;
10 |
11 | input {
12 | height: 100%;
13 | width: 100%;
14 | line-height: 100%;
15 | font-size: 1.5rem;
16 | padding: .5rem;
17 | border-radius: 5px;
18 | border-width: 1px;
19 |
20 | &:focus,
21 | &:active {
22 | border-width: 2px;
23 | border-color: white;
24 | }
25 |
26 | &::before,
27 | &::after {
28 | border: none;
29 | }
30 | }
31 | }
32 |
33 | li {
34 | list-style: none;
35 | padding: .2rem 0;
36 |
37 | a {
38 | display: flex;
39 | flex-direction: row;
40 | justify-content: space-between;
41 | }
42 | }
43 |
44 | .qtot {
45 | position: absolute;
46 | right: 1.5rem;
47 | top: 0.5rem;
48 | user-select: none;
49 | }
50 |
51 | .tag {
52 | cursor: pointer;
53 | background: $gray-100;
54 | user-select: none;
55 |
56 | &.sm {
57 | color: black !important;
58 | font-size: .875rem;
59 | padding: 2px 5px;
60 | }
61 |
62 | &:hover {
63 | background: var(--bs-btn-active-bg) !important;
64 | }
65 |
66 | &.active {
67 | background: $orange-200 !important;
68 | transition: all ease-in-out 100ms;
69 | font-weight: 600;
70 | }
71 |
72 | &.active1 {
73 | background: $gray-200 !important;
74 | transition: all ease-in-out 100ms;
75 | font-weight: bold;
76 | }
77 | }
78 |
79 |
80 | .search-table {
81 | width: 100%;
82 |
83 | border-collapse: separate;
84 |
85 | border-radius: 15px;
86 | box-shadow: none;
87 |
88 | padding: 1rem;
89 | display: flex;
90 | flex-direction: column;
91 | text-align: center;
92 |
93 | .text-left {
94 | text-align: left;
95 | }
96 |
97 | *,
98 | ::after,
99 | ::before {
100 | border: 0 solid #e5e7eb;
101 | box-sizing: border-box;
102 | box-shadow: none;
103 | }
104 |
105 | .table-head {
106 | background: $gray-500;
107 | border-top-left-radius: 15px;
108 | border-top-right-radius: 15px;
109 | margin-bottom: 15px;
110 | }
111 |
112 | .table-body {
113 | display: flex;
114 | flex-direction: column;
115 | gap: 10px;
116 | position: relative;
117 | width: 100%;
118 | }
119 |
120 | .table-row {
121 | border-radius: 15px;
122 | display: grid;
123 | grid-template-columns: 1fr 4fr 2fr 4fr;
124 | padding: 5px 10px;
125 | width: 100%;
126 | height: auto;
127 | min-height: 72px;
128 | cursor: pointer;
129 |
130 | td {
131 | height: auto;
132 | }
133 | }
134 |
135 | .bg-color {
136 | background: $gray-100;
137 |
138 | &:hover {
139 | background: rgb(229, 229, 229);
140 | transition: all ease-in-out 100ms;
141 | }
142 | }
143 | }
144 | }
145 |
146 | @include media-breakpoint-down(sm) {
147 | .search {
148 | padding: 10px;
149 |
150 | .search-table {
151 | border-radius: 0;
152 | border: none;
153 | padding: 0;
154 | }
155 | }
156 | }
157 |
158 | @include color-mode(dark, false) {
159 | .search {
160 | td {
161 | color: $gray-100;
162 | }
163 | .tag {
164 | background: black;
165 | color: white;
166 | }
167 | .bg-color {
168 | background: $gray-800;
169 | &:hover {
170 | background: $gray-300;
171 | transition: all ease-in-out 100ms;
172 | }
173 | }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/scss/_zen.scss:
--------------------------------------------------------------------------------
1 | .zen-container {
2 | min-width: 60%;
3 | }
4 |
5 | .zen-nav {
6 | position: sticky;
7 | top: 0;
8 | z-index: 1000;
9 | gap: 1rem;
10 | }
11 |
12 | .zen-table-row {
13 | :hover {
14 | cursor: pointer;
15 | }
16 | td {
17 | vertical-align: middle;
18 | text-align: center;
19 | padding: 0.25rem!important;
20 | }
21 |
22 | .zen-spinner-td {
23 | text-align: center;
24 | }
25 |
26 | .zen-ans {
27 | float: right;
28 |
29 | a {
30 | color: black!important;
31 | font-size: 1.5rem!important;
32 | font-weight: normal;
33 | text-decoration: none;
34 | &:hover {
35 | transform: scale(1.2);
36 | }
37 | }
38 | }
39 | }
40 |
41 | .zen-table-header {
42 | .zen-table-header-no {
43 | width: 100px;
44 | }
45 |
46 | .zen-table-header-question {
47 | width: 400px;
48 | }
49 |
50 | .zen-table-header-progress {
51 | width: 250px;
52 | }
53 | }
54 |
55 | .zen-filter-dialog {
56 | margin-top: 5%!important;
57 |
58 | .zen-filter-tag {
59 | .active {
60 | background: plum;
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/scss/algorithm/styles.scss:
--------------------------------------------------------------------------------
1 | @import "../bs";
2 | @import "katex/dist/katex.css";
3 |
4 | code {
5 | padding: 1rem;
6 | }
7 | a {
8 | text-underline-offset: .2rem;
9 | text-decoration-style: dashed;
10 | text-decoration-color: rgb(240, 99, 99);
11 | text-decoration-thickness: 1px;
12 | }
13 | a:visited {
14 | color: initial;
15 | }
16 |
17 | blockquote {
18 | border-left: 5px solid rgb(255, 225, 0);
19 | background: rgb(240, 240, 240);
20 | }
21 |
22 | *[data-rehype-pretty-code-figure] {
23 | margin-block: 0;
24 | margin-inline: 0;
25 | }
26 |
27 | .debug * {
28 | // outline: 1px dashed gold;
29 | }
30 |
31 | .mdx-layout {
32 | display: grid;
33 | grid-template-areas: "top-nav top-nav" "side-nav content";
34 | grid-template-columns: 2fr 5fr;
35 | // grid-template-rows: 48px auto;
36 | grid-auto-rows: max-content;
37 | height: 100vh;
38 | width: 100%;
39 |
40 | .side-nav {
41 | grid-area: side-nav;
42 | padding: 1rem;
43 | position: fixed;
44 | top: 48px;
45 | height: auto;
46 | margin-bottom: auto;
47 | ul {
48 | margin-inline: 0;
49 | padding-inline: 0;
50 | margin-block: 0;
51 | gap: .5rem;
52 | display: flex;
53 | flex-direction: column;
54 | }
55 |
56 | li {
57 | list-style: none;
58 | cursor: pointer;
59 | }
60 |
61 | .menu-item {
62 | text-align: left;
63 | padding: .5rem;
64 | transition: all ease-in-out 200ms;
65 | &:hover {
66 | opacity: .8;
67 | }
68 |
69 | font-size: 1.2rem;
70 | text-decoration: none;
71 | user-select: none;
72 | transition: all ease-in-out 200ms;
73 | $active-color: rgb(222, 84, 84);
74 |
75 | &.active {
76 | color: $active-color;
77 | text-decoration: underline $active-color;
78 | text-underline-offset: .2rem;
79 | }
80 | }
81 | }
82 |
83 | .top-nav {
84 | grid-area: top-nav;
85 | text-align: center;
86 | font-size: 2rem;
87 | }
88 |
89 | .content {
90 | grid-area: content;
91 | padding: 1rem;
92 | font-size: 20px;
93 | margin: 0 auto;
94 | width: 100%;
95 |
96 | & * {
97 | outline: none;
98 | }
99 | }
100 | }
101 |
102 | @include media-breakpoint-up(lg) {
103 | .mdx-layout {
104 | width: 65%;
105 | margin: 0 auto;
106 | }
107 | }
108 |
109 |
--------------------------------------------------------------------------------
/scss/styles.scss:
--------------------------------------------------------------------------------
1 | // app styles
2 | @import "./common";
3 | @import "./search";
4 | @import "./zen";
5 | @import "./search";
6 | @import "./list";
7 |
8 | // components
9 | @import "@components/ProblemCatetory/_index";
10 |
11 | nav {
12 | z-index: 1000;
13 | }
14 | // color mode
15 | @include color-mode(dark, false) {
16 | nav {
17 | background: $gray-900;
18 | }
19 |
20 | #nav-cl,#nav-tr,#nav-0x3f,#nav-pg,#nav-pl {
21 | background: var(--bs-gray-700);
22 | }
23 | }
24 |
25 | @include color-mode(light, false) {
26 | nav {
27 | background: $gray-100;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tags.json:
--------------------------------------------------------------------------------
1 | [[28514800,"Radix Sort","基数排序"],[37026620,"Combinatorics","组合数学"],[42980886,"Simulation","模拟"],[156137976,"Union Find","并查集"],[300022785,"Shell","Shell"],[326262884,"Counting","计数"],[326993225,"Segment Tree","线段树"],[365099244,"Matrix","矩阵"],[398550328,"String","字符串"],[464032360,"Bitmask","状态压缩"],[477549556,"Eulerian Circuit","欧拉回路"],[587523799,"Brainteaser","脑筋急转弯"],[601298120,"Data Stream","数据流"],[631064497,"Biconnected Component","双连通分量"],[711820689,"Geometry","几何"],[1110971868,"Reservoir Sampling","水塘抽样"],[1157090682,"Line Sweep","扫描线"],[1194511624,"Randomized","随机化"],[1217109157,"Shortest Path","最短路"],[1271117903,"Iterator","迭代器"],[1288014335,"Binary Tree","二叉树"],[1388774735,"Sorting","排序"],[1431509416,"Suffix Array","后缀数组"],[1438644433,"Strongly Connected Component","强连通分量"],[1463482908,"Enumeration","枚举"],[1503330480,"String Matching","字符串匹配"],[1562005820,"Hash Function","哈希函数"],[1649501183,"Stack","栈"],[1713688490,"Dynamic Programming","动态规划"],[1736422075,"Minimum Spanning Tree","最小生成树"],[1837839573,"Tree","树"],[1855906840,"Concurrency","多线程"],[1969930655,"Prefix Sum","前缀和"],[1988863599,"Topological Sort","拓扑排序"],[1991642727,"Recursion","递归"],[1992185659,"Binary Search","二分查找"],[2106869857,"Trie","字典树"],[2115531193,"Rolling Hash","滚动哈希"],[2130700395,"Interactive","交互"],[2201204921,"Greedy","贪心"],[2242663311,"Backtracking","回溯"],[2302066520,"Divide and Conquer","分治"],[2321067302,"Array","数组"],[2364557342,"Binary Search Tree","二叉搜索树"],[2412173278,"Bucket Sort","桶排序"],[2425219275,"Binary Indexed Tree","树状数组"],[2476338297,"Monotonic Queue","单调队列"],[2494469528,"Depth-First Search","深度优先搜索"],[2705407897,"Memoization","记忆化搜索"],[2707807672,"Database","数据库"],[2793020174,"Merge Sort","归并排序"],[2822699490,"Bit Manipulation","位运算"],[2879114835,"Counting Sort","计数排序"],[2896989895,"Ordered Set","有序集合"],[2960422376,"Heap (Priority Queue)","堆(优先队列)"],[3027943496,"Hash Table","哈希表"],[3031138664,"Sliding Window","滑动窗口"],[3111650887,"Graph","图"],[3222202221,"Doubly-Linked List","双向链表"],[3304615908,"Quickselect","快速选择"],[3656873726,"Monotonic Stack","单调栈"],[3682322655,"Linked List","链表"],[3837742759,"Game Theory","博弈"],[3861735881,"Breadth-First Search","广度优先搜索"],[3986329634,"Number Theory","数论"],[4001929615,"Math","数学"],[4049367376,"Probability and Statistics","概率与统计"],[4108302520,"Queue","队列"],[4115727122,"Two Pointers","双指针"],[4214343119,"Design","设计"],[4291276582,"Rejection Sampling","拒绝采样"]]
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "baseUrl": ".",
10 | "paths": {
11 | "@*": ["./*"]
12 | },
13 | "allowJs": true,
14 | "skipLibCheck": true,
15 | "strict": false,
16 | "forceConsistentCasingInFileNames": true,
17 | "noEmit": true,
18 | "esModuleInterop": true,
19 | "module": "esnext",
20 | "moduleResolution": "node",
21 | "resolveJsonModule": true,
22 | "isolatedModules": true,
23 | "jsx": "preserve",
24 | "incremental": true,
25 | "plugins": [
26 | {
27 | "name": "next"
28 | }
29 | ]
30 | },
31 | "include": [
32 | "next-env.d.ts",
33 | "**/*.ts",
34 | "**/*.tsx",
35 | ".next/types/**/*.ts",
36 | "dist/types/**/*.ts",
37 | "build/types/**/*.ts"
38 | ],
39 | "exclude": [
40 | "node_modules"
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/utils/debounce.ts:
--------------------------------------------------------------------------------
1 | function debounce void>(
2 | func: T,
3 | wait: number,
4 | immediate: boolean = false
5 | ): (...args: Parameters) => void {
6 | let handle: ReturnType | undefined = undefined;
7 |
8 | return (...args: Parameters): void => {
9 | const shouldCallNow = immediate && handle === undefined;
10 |
11 | if (handle !== undefined) {
12 | clearTimeout(handle);
13 | }
14 |
15 | handle = setTimeout(() => {
16 | if (!immediate) {
17 | func(...args);
18 | }
19 | handle = undefined;
20 | }, wait);
21 |
22 | if (shouldCallNow) {
23 | func(...args);
24 | }
25 | };
26 | }
27 |
28 | export default debounce;
29 |
--------------------------------------------------------------------------------
/utils/hash.ts:
--------------------------------------------------------------------------------
1 | import { BinaryLike, createHash } from "crypto";
2 |
3 | export function hashCode(data: BinaryLike) {
4 | return createHash("md5").update(data).digest("hex");
5 | }
6 |
--------------------------------------------------------------------------------
/utils/throttle.ts:
--------------------------------------------------------------------------------
1 | function throttle void>(
2 | func: T,
3 | wait: number
4 | ): (...args: Parameters) => void {
5 | let prev = 0;
6 |
7 | return (...args: Parameters): void => {
8 | const now = Date.now();
9 |
10 | if (now - prev >= wait) {
11 | func(...args);
12 | prev = now;
13 | }
14 | };
15 | }
16 |
17 | export default throttle;
18 |
--------------------------------------------------------------------------------