Skip to content

Commit d20552b

Browse files
authored
Merge pull request #79 from ut-code/copilot/mark-current-section-in-sidebar
Highlight current section in sidebar table of contents
2 parents 66a9ca4 + 4850b07 commit d20552b

File tree

4 files changed

+112
-50
lines changed

4 files changed

+112
-50
lines changed

app/[docs_id]/dynamicMdContext.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"use client";
2+
3+
import { createContext, ReactNode, useContext, useState } from "react";
4+
import { DynamicMarkdownSection } from "./pageContent";
5+
6+
export interface ISidebarMdContext {
7+
sidebarMdContent: DynamicMarkdownSection[];
8+
setSidebarMdContent: React.Dispatch<React.SetStateAction<DynamicMarkdownSection[]>>;
9+
}
10+
11+
const SidebarMdContext = createContext<ISidebarMdContext | null>(null);
12+
13+
export function useSidebarMdContext() {
14+
const context = useContext(SidebarMdContext);
15+
if (!context) {
16+
throw new Error(
17+
"useSidebarMdContext must be used within a SidebarMdProvider"
18+
);
19+
}
20+
return context;
21+
}
22+
23+
export function useSidebarMdContextOptional() {
24+
return useContext(SidebarMdContext);
25+
}
26+
27+
export function SidebarMdProvider({
28+
children,
29+
}: {
30+
children: ReactNode;
31+
}) {
32+
const [sidebarMdContent, setSidebarMdContent] =
33+
useState<DynamicMarkdownSection[]>([]);
34+
35+
return (
36+
<SidebarMdContext.Provider value={{ sidebarMdContent, setSidebarMdContent }}>
37+
{children}
38+
</SidebarMdContext.Provider>
39+
);
40+
}

app/[docs_id]/pageContent.tsx

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { MarkdownSection } from "./splitMarkdown";
55
import { ChatForm } from "./chatForm";
66
import { Heading, StyledMarkdown } from "./markdown";
77
import { useChatHistoryContext } from "./chatHistory";
8+
import { useSidebarMdContext } from "./dynamicMdContext";
89
import clsx from "clsx";
910

1011
// MarkdownSectionに追加で、ユーザーが今そのセクションを読んでいるかどうか、などの動的な情報を持たせる
@@ -19,26 +20,34 @@ interface PageContentProps {
1920
docs_id: string;
2021
}
2122
export function PageContent(props: PageContentProps) {
23+
const { setSidebarMdContent } = useSidebarMdContext();
24+
25+
// SSR用のローカルstate
2226
const [dynamicMdContent, setDynamicMdContent] = useState<
2327
DynamicMarkdownSection[]
2428
>(
25-
// useEffectで更新するのとは別に、SSRのための初期値
2629
props.splitMdContent.map((section, i) => ({
2730
...section,
2831
inView: false,
2932
sectionId: `${props.docs_id}-${i}`,
3033
}))
3134
);
35+
3236
useEffect(() => {
33-
// props.splitMdContentが変わったときにdynamicMdContentを更新
34-
setDynamicMdContent(
35-
props.splitMdContent.map((section, i) => ({
36-
...section,
37-
inView: false,
38-
sectionId: `${props.docs_id}-${i}`,
39-
}))
40-
);
41-
}, [props.splitMdContent, props.docs_id]);
37+
// props.splitMdContentが変わったときにローカルstateとcontextの両方を更新
38+
const newContent = props.splitMdContent.map((section, i) => ({
39+
...section,
40+
inView: false,
41+
sectionId: `${props.docs_id}-${i}`,
42+
}));
43+
setDynamicMdContent(newContent);
44+
setSidebarMdContent(newContent);
45+
46+
// クリーンアップ:コンポーネントがアンマウントされたらcontextをクリア
47+
return () => {
48+
setSidebarMdContent([]);
49+
};
50+
}, [props.splitMdContent, props.docs_id, setSidebarMdContent]);
4251

4352
const sectionRefs = useRef<Array<HTMLDivElement | null>>([]);
4453
// sectionRefsの長さをsplitMdContentに合わせる
@@ -48,7 +57,7 @@ export function PageContent(props: PageContentProps) {
4857

4958
useEffect(() => {
5059
const handleScroll = () => {
51-
setDynamicMdContent((prevDynamicMdContent) => {
60+
const updateContent = (prevDynamicMdContent: DynamicMarkdownSection[]) => {
5261
const dynMdContent = prevDynamicMdContent.slice(); // Reactの変更検知のために新しい配列を作成
5362
for (let i = 0; i < sectionRefs.current.length; i++) {
5463
if (sectionRefs.current.at(i) && dynMdContent.at(i)) {
@@ -58,14 +67,18 @@ export function PageContent(props: PageContentProps) {
5867
}
5968
}
6069
return dynMdContent;
61-
});
70+
};
71+
72+
// ローカルstateとcontextの両方を更新
73+
setDynamicMdContent(updateContent);
74+
setSidebarMdContent(updateContent);
6275
};
6376
window.addEventListener("scroll", handleScroll);
6477
handleScroll();
6578
return () => {
6679
window.removeEventListener("scroll", handleScroll);
6780
};
68-
}, []);
81+
}, [setSidebarMdContent]);
6982

7083
const [isFormVisible, setIsFormVisible] = useState(false);
7184

app/layout.tsx

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { PyodideProvider } from "./terminal/python/pyodide";
99
import { WandboxProvider } from "./terminal/wandbox/wandbox";
1010
import { EmbedContextProvider } from "./terminal/embedContext";
1111
import { AutoAnonymousLogin } from "./accountMenu";
12+
import { SidebarMdProvider } from "./[docs_id]/dynamicMdContext";
1213

1314
export const metadata: Metadata = {
1415
title: "Create Next App",
@@ -22,25 +23,27 @@ export default function RootLayout({
2223
<html lang="ja">
2324
<body className="w-screen h-screen">
2425
<AutoAnonymousLogin />
25-
<div className="drawer lg:drawer-open">
26-
<input id="drawer-toggle" type="checkbox" className="drawer-toggle" />
27-
<div className="drawer-content flex flex-col">
28-
<Navbar />
29-
<EmbedContextProvider>
30-
<PyodideProvider>
31-
<WandboxProvider>{children}</WandboxProvider>
32-
</PyodideProvider>
33-
</EmbedContextProvider>
26+
<SidebarMdProvider>
27+
<div className="drawer lg:drawer-open">
28+
<input id="drawer-toggle" type="checkbox" className="drawer-toggle" />
29+
<div className="drawer-content flex flex-col">
30+
<Navbar />
31+
<EmbedContextProvider>
32+
<PyodideProvider>
33+
<WandboxProvider>{children}</WandboxProvider>
34+
</PyodideProvider>
35+
</EmbedContextProvider>
36+
</div>
37+
<div className="drawer-side shadow-md z-50">
38+
<label
39+
htmlFor="drawer-toggle"
40+
aria-label="close sidebar"
41+
className="drawer-overlay"
42+
/>
43+
<Sidebar />
44+
</div>
3445
</div>
35-
<div className="drawer-side shadow-md z-50">
36-
<label
37-
htmlFor="drawer-toggle"
38-
aria-label="close sidebar"
39-
className="drawer-overlay"
40-
/>
41-
<Sidebar />
42-
</div>
43-
</div>
46+
</SidebarMdProvider>
4447
</body>
4548
</html>
4649
);

app/sidebar.tsx

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
"use client";
22
import Link from "next/link";
33
import { usePathname } from "next/navigation";
4-
import useSWR, { Fetcher } from "swr";
5-
import { splitMarkdown } from "./[docs_id]/splitMarkdown";
64
import { pagesList } from "./pagesList";
75
import { AccountMenu } from "./accountMenu";
86
import { ThemeToggle } from "./[docs_id]/themeToggle";
9-
10-
const fetcher: Fetcher<string, string> = (url) =>
11-
fetch(url).then((r) => r.text());
7+
import { useSidebarMdContext } from "./[docs_id]/dynamicMdContext";
128

139
export function Sidebar() {
1410
const pathname = usePathname();
1511
const docs_id = pathname.replace(/^\//, "");
16-
const { data, error, isLoading } = useSWR(`/docs/${docs_id}.md`, fetcher);
17-
18-
if (error) console.error("Sidebar fetch error:", error);
19-
20-
const splitmdcontent = splitMarkdown(data ?? "");
12+
const { sidebarMdContent } = useSidebarMdContext();
13+
14+
// 現在表示中のセクション(最初にinViewがtrueのもの)を見つける
15+
const currentSectionIndex = sidebarMdContent.findIndex(
16+
(section) => section.inView
17+
);
2118
return (
2219
<div className="bg-base-200 h-full w-80 overflow-y-auto">
2320
{/* todo: 背景色ほんとにこれでいい? */}
@@ -65,16 +62,25 @@ export function Sidebar() {
6562
<span className="mr-0">{page.id}.</span>
6663
{page.title}
6764
</Link>
68-
{`${group.id}-${page.id}` === docs_id && !isLoading && (
65+
{`${group.id}-${page.id}` === docs_id && sidebarMdContent.length > 0 && (
6966
<ul className="ml-4 text-sm">
70-
{splitmdcontent.slice(1).map((section, idx) => (
71-
<li
72-
key={idx}
73-
style={{ marginLeft: section.level - 2 + "em" }}
74-
>
75-
<Link href={`#${idx + 1}`}>{section.title}</Link>
76-
</li>
77-
))}
67+
{sidebarMdContent.slice(1).map((section, idx) => {
68+
// idx + 1 は実際のsectionIndexに対応(slice(1)で最初を除外しているため)
69+
const isCurrentSection = idx + 1 === currentSectionIndex;
70+
return (
71+
<li
72+
key={idx}
73+
style={{ marginLeft: section.level - 2 + "em" }}
74+
>
75+
<Link
76+
href={`#${idx + 1}`}
77+
className={isCurrentSection ? "font-bold" : ""}
78+
>
79+
{section.title}
80+
</Link>
81+
</li>
82+
);
83+
})}
7884
</ul>
7985
)}
8086
</li>

0 commit comments

Comments
 (0)