├── .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 | GitHub 4 | GitHub commit activity (branch) 5 | GitHub package.json version (branch) 6 | GitHub Workflow Status (with event) 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 | Git Icon 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 | Git Icon 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 | 13 | 18 | 19 | 20 | 29 | 30 | 31 | 32 | 33 | 34 | 39 | 47 | 48 | 49 | 54 | 63 | 64 | 65 | 70 | 79 | 80 | 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 |
    78 | 79 | {sortedFormData.map((item, i) => ( 80 | 81 | 82 | 83 | handleFieldChange(e, i, "key")} 87 | isInvalid={errors[i] !== undefined} 88 | disabled={i < Object.keys(defaultOptions).length} 89 | /> 90 | {errors[i] && ( 91 | 92 | {errors[i]} 93 | 94 | )} 95 | 96 | 97 | 98 | 99 | handleFieldChange(e, i, "label")} 103 | /> 104 | 105 | 106 | 107 | 108 | handleFieldChange(e, i, "color")} 114 | /> 115 | 123 | 124 | 125 | 126 | ))} 127 | 128 | 129 | 132 | 135 | 136 | 137 |
    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 | 20 | 21 | 22 | ) : ( 23 | 30 | 34 | 38 | 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 |
    141 | 142 |
    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 | 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 | 197 | ); 198 | })} 199 | 200 | ))} 201 | 202 |
    176 | {flexRender( 177 | header.column.columnDef.header, 178 | header.getContext() 179 | )} 180 |
    195 | {flexRender(cell.column.columnDef.cell, context)} 196 |
    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 | 11 | 15 | 19 | 20 | ); 21 | }; 22 | 23 | export const ShareIcon = (props: any) => { 24 | return ( 25 | 33 | 37 | 38 | ); 39 | }; 40 | 41 | export const TodoIcon = (props: any) => { 42 | return ( 43 | 51 | 55 | 56 | ); 57 | }; 58 | 59 | export const RandomIcon = (props: any) => { 60 | return ( 61 | 69 | 74 | 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'{alt}' 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 | --------------------------------------------------------------------------------