Skip to content

Commit

Permalink
增加进度选项的自定义功能 (#51)
Browse files Browse the repository at this point in the history
* fix: 修复进度下拉选项颜色错误

* refactor: 增加站点设置组件,将同步题目进度组件迁移至设置组件内部

* optim: 优化useQuestProgress,实现全局共享数据

* refactor: ProblemCatetory.tsx拆分到多个文件

* feature: 新增自定义选项的功能

* refactor: 用useSyncExternalStore重写useStorage,实现同标签页组件共享相同state

* fix: 修复Form.Control在Link内部导致颜色选择器无法正常显示的问题

* feature: 实时预览进度选项自定义效果
  • Loading branch information
Autumnal-Joy authored Mar 6, 2025
1 parent a96bae9 commit 69c3e02
Show file tree
Hide file tree
Showing 21 changed files with 955 additions and 490 deletions.
157 changes: 157 additions & 0 deletions components/ProblemCatetory/ProblemCategoryList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { ShareIcon } from "@components/icons";
import RatingCircle, { ColorRating } from "@components/RatingCircle";
import {
OptionEntry,
ProgressKeyType,
useProgressOptions,
useQuestProgress,
} from "@hooks/useProgress";
import { hashCode } from "@utils/hash";
import Form from "react-bootstrap/esm/Form";

const getCols = (l: number) => {
if (l < 12) {
return "";
}
if (l < 20) {
return "col2";
}
return "col3";
};

const title2id = (title: string) => {
// title: number. title
return title.split(". ")[0];
};

interface ProblemCategory {
title: string;
summary?: string;
src?: string;
original_src?: string;
sort?: Number;
isLeaf?: boolean;
solution?: string | null;
score?: Number | null;
child?: ProblemCategory[];
isPremium?: boolean;
last_update?: string;
}

interface ProblemCategoryListProps {
optionKeys: ProgressKeyType[];
getOption: (key?: ProgressKeyType) => OptionEntry;
allProgress: Record<string, ProgressKeyType>;
updateProgress: (questID: string, progress: ProgressKeyType) => void;
removeProgress: (questID: string) => void;
data: ProblemCategory;
showEn?: boolean;
showRating?: boolean;
showPremium?: boolean;
}

function ProblemCategoryList({
optionKeys,
getOption,
allProgress,
updateProgress,
removeProgress,
data,
showEn,
showRating,
showPremium,
}: ProblemCategoryListProps) {
// Event handlers
const handleProgressSelectChange = (
questID: string,
progress: ProgressKeyType
) => {
if (progress === getOption().key) {
removeProgress(questID);
} else {
updateProgress(questID, progress);
}
};

return (
<div className="shadow rounded p-2 leaf">
<h3 className="title" id={`${hashCode(data.title || "")}`}>
{data.title}
</h3>
{data.summary && (
<p
className="p-2 rounded summary bg-secondary-subtle text-warning-emphasis"
dangerouslySetInnerHTML={{ __html: data.summary }}
></p>
)}
<ul className={`list p-2 ${data.child && getCols(data.child.length)}`}>
{data.child &&
data.child
.filter((item) => !item.isPremium || showPremium)
.map((item) => {
const id = title2id(item.title);
const progressKey = allProgress[id];
const option = getOption(progressKey);

const rating = Number(item.score);

return (
<li
className="d-flex justify-content-between"
key={hashCode(item.title || "")}
>
<div>
<a
href={"https://leetcode.cn/problems" + item.src}
target="_blank"
>
{item.title + (item.isPremium ? " (会员题)" : "")}
</a>
{showEn && (
<a
className="ms-2"
href={"https://leetcode.com/problems" + item.src}
target="_blank"
>
<ShareIcon height={16} width={16} />
</a>
)}
</div>
{item.score && showRating ? (
<div className="ms-2 text-nowrap d-flex justify-content-center align-items-center pb-rating-bg">
<RatingCircle rating={rating} />
<ColorRating className="rating-text" rating={rating}>
{rating.toFixed(0)}
</ColorRating>
</div>
) : null}
<div className="d-flex align-items-center ms-2">
<Form.Select
style={{
color: option.color,
}}
value={option.key}
onChange={(e) =>
handleProgressSelectChange(id, e.target.value)
}
>
{optionKeys.map((p) => (
<option
key={p}
value={p}
style={{ color: getOption(p).color }}
>
{getOption(p).label}
</option>
))}
</Form.Select>
</div>
</li>
);
})}
</ul>
</div>
);
}

export default ProblemCategoryList;
162 changes: 10 additions & 152 deletions components/ProblemCatetory/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import { ShareIcon } from "@components/icons";
import RatingCircle, { ColorRating } from "@components/RatingCircle";
import { ProgressKeyType, useProgressOptions } from "@hooks/useProgress";
import { hashCode } from "@utils/hash";
import { useCallback, useState } from "react";
import Form from "react-bootstrap/esm/Form";

const LC_RATING_PROGRESS_KEY = (questionID: string) =>
`lc-rating-zen-progress-${questionID}`;

// Progress Related
type ProgressData = Record<string, string>;
import ProblemCategoryList from "./ProblemCategoryList";
import { useProgressOptions, useQuestProgress } from "@hooks/useProgress";

interface ProblemCategory {
title: string;
Expand All @@ -36,22 +27,6 @@ interface ProblemCategoryProps {
showPremium?: boolean;
}

function count(data: ProblemCategory[] | undefined) {
if (!data) {
return 0;
}

let tot = 0;
for (let i = 0; i < data.length; i++) {
if (!data[i].isLeaf) {
tot += count(data[i].child);
} else {
tot += data[i].child?.length || 0;
}
}
return tot;
}

function ProblemCategory({
title,
summary,
Expand All @@ -62,6 +37,9 @@ function ProblemCategory({
showRating,
showPremium,
}: ProblemCategoryProps) {
const { optionKeys, getOption } = useProgressOptions();
const { allProgress, updateProgress, removeProgress } = useQuestProgress();

return (
<div className={`pb-container level-${level}` + className}>
{
Expand All @@ -85,11 +63,15 @@ function ProblemCategory({
>
{item.isLeaf ? (
<ProblemCategoryList
optionKeys={optionKeys}
getOption={getOption}
allProgress={allProgress}
updateProgress={updateProgress}
removeProgress={removeProgress}
showEn={showEn}
showRating={showRating}
showPremium={showPremium}
data={item}
className={`leaf`}
/>
) : (
item.child &&
Expand All @@ -114,128 +96,4 @@ function ProblemCategory({
);
}

function ProblemCategoryList({
data,
className = "",
showEn,
showRating,
showPremium,
}: {
data: ProblemCategory;
className?: string;
showEn?: boolean;
showRating?: boolean;
showPremium?: boolean;
}) {
const getCols = (l: number) => {
if (l < 12) {
return "";
}
if (l < 20) {
return "col2";
}
return "col3";
};
const { optionKeys, getOption } = useProgressOptions();
// trigger page to refresh
const [localStorageProgressData, setLocalStorageProgressData] =
useState<ProgressData>({});

// Event handlers
const handleProgressSelectChange = useCallback(
(questionId: string, value: ProgressKeyType) => {
localStorage.setItem(LC_RATING_PROGRESS_KEY(questionId), value);
setLocalStorageProgressData((prevData) => ({
...prevData,
[questionId]: value,
}));
},
[]
);

const title2id = (title: string) => {
// title: number. title
return title.split(". ")[0];
};

const progress = (title: string) => {
const localtemp = localStorage.getItem(
LC_RATING_PROGRESS_KEY(title2id(title))
) as ProgressKeyType;
return localtemp;
};

return (
<div className="shadow rounded p-2 leaf">
<h3 className="title" id={`${hashCode(data.title || "")}`}>
{data.title}
</h3>
{data.summary && (
<p
className="p-2 rounded summary bg-secondary-subtle text-warning-emphasis"
dangerouslySetInnerHTML={{ __html: data.summary }}
></p>
)}
<ul className={`list p-2 ${data.child && getCols(data.child.length)}`}>
{data.child &&
data.child
.filter((item) => !item.isPremium || showPremium)
.map((item) => (
<li
className="d-flex justify-content-between"
key={hashCode(item.title || "")}
>
<div>
<a
href={"https://leetcode.cn/problems" + item.src}
target="_blank"
>
{item.title + (item.isPremium ? " (会员题)" : "")}
</a>
{showEn && (
<a
className="ms-2"
href={"https://leetcode.com/problems" + item.src}
target="_blank"
>
<ShareIcon height={16} width={16} />
</a>
)}
</div>
{item.score && showRating ? (
<div className="ms-2 text-nowrap d-flex justify-content-center align-items-center pb-rating-bg">
<RatingCircle rating={Number(item.score)} />
<ColorRating
className="rating-text"
rating={Number(item.score)}
>
{Number(item.score).toFixed(0)}
</ColorRating>
</div>
) : null}
<div className="d-flex align-items-center ms-2">
<Form.Select
style={{ color: getOption(progress(item.title)).color }}
value={getOption(progress(item.title)).key}
onChange={(e) =>
handleProgressSelectChange(
title2id(item.title),
e.target.value
)
}
>
{optionKeys.map((p) => (
<option key={getOption(p).key} value={getOption(p).key}>
{getOption(p).label}
</option>
))}
</Form.Select>
</div>
</li>
))}
</ul>
</div>
);
}

export default ProblemCategory;
29 changes: 29 additions & 0 deletions components/SettingsPanel/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Nav } from "react-bootstrap";
import { SettingTabType } from "./config";

interface SidebarProps {
tabs: SettingTabType[];
activeTab: string;
onTabChange: (key: string) => void;
}

const Sidebar = ({ tabs, activeTab, onTabChange }: SidebarProps) => {
return (
<Nav variant="pills" className="flex-column sticky-top">
{tabs.map((tab) => (
<Nav.Item key={tab.key}>
<Nav.Link
active={activeTab === tab.key}
onClick={() => onTabChange(tab.key)}
className="cursor-pointer sidebar-link text-start"
>
<span className="mx-2">{tab.icon}</span>
{tab.title}
</Nav.Link>
</Nav.Item>
))}
</Nav>
);
};

export default Sidebar;
Loading

0 comments on commit 69c3e02

Please sign in to comment.