Skip to content

Commit d0c4523

Browse files
Copilotna-trium-144
andcommitted
Add context to share dynamicMdContent and highlight current section in sidebar
Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com>
1 parent 9911330 commit d0c4523

4 files changed

Lines changed: 93 additions & 27 deletions

File tree

app/[docs_id]/dynamicMdContext.tsx

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

app/[docs_id]/page.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { MarkdownSection, splitMarkdown } from "./splitMarkdown";
66
import pyodideLock from "pyodide/pyodide-lock.json";
77
import { PageContent } from "./pageContent";
88
import { ChatHistoryProvider } from "./chatHistory";
9+
import { DynamicMdProvider } from "./dynamicMdContext";
910
import { getChat } from "@/lib/chatHistory";
1011

1112
export default async function Page({
@@ -52,13 +53,21 @@ export default async function Page({
5253

5354
const initialChatHistories = getChat(docs_id);
5455

56+
const initialDynamicMdContent = (await splitMdContent).map((section, i) => ({
57+
...section,
58+
inView: false,
59+
sectionId: `${docs_id}-${i}`,
60+
}));
61+
5562
return (
5663
<ChatHistoryProvider initialChatHistories={await initialChatHistories}>
57-
<PageContent
58-
documentContent={await mdContent}
59-
splitMdContent={await splitMdContent}
60-
docs_id={docs_id}
61-
/>
64+
<DynamicMdProvider initialContent={initialDynamicMdContent}>
65+
<PageContent
66+
documentContent={await mdContent}
67+
splitMdContent={await splitMdContent}
68+
docs_id={docs_id}
69+
/>
70+
</DynamicMdProvider>
6271
</ChatHistoryProvider>
6372
);
6473
}

app/[docs_id]/pageContent.tsx

Lines changed: 5 additions & 12 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 { useDynamicMdContext } from "./dynamicMdContext";
89
import clsx from "clsx";
910

1011
// MarkdownSectionに追加で、ユーザーが今そのセクションを読んでいるかどうか、などの動的な情報を持たせる
@@ -19,16 +20,8 @@ interface PageContentProps {
1920
docs_id: string;
2021
}
2122
export function PageContent(props: PageContentProps) {
22-
const [dynamicMdContent, setDynamicMdContent] = useState<
23-
DynamicMarkdownSection[]
24-
>(
25-
// useEffectで更新するのとは別に、SSRのための初期値
26-
props.splitMdContent.map((section, i) => ({
27-
...section,
28-
inView: false,
29-
sectionId: `${props.docs_id}-${i}`,
30-
}))
31-
);
23+
const { dynamicMdContent, setDynamicMdContent } = useDynamicMdContext();
24+
3225
useEffect(() => {
3326
// props.splitMdContentが変わったときにdynamicMdContentを更新
3427
setDynamicMdContent(
@@ -38,7 +31,7 @@ export function PageContent(props: PageContentProps) {
3831
sectionId: `${props.docs_id}-${i}`,
3932
}))
4033
);
41-
}, [props.splitMdContent, props.docs_id]);
34+
}, [props.splitMdContent, props.docs_id, setDynamicMdContent]);
4235

4336
const sectionRefs = useRef<Array<HTMLDivElement | null>>([]);
4437
// sectionRefsの長さをsplitMdContentに合わせる
@@ -65,7 +58,7 @@ export function PageContent(props: PageContentProps) {
6558
return () => {
6659
window.removeEventListener("scroll", handleScroll);
6760
};
68-
}, []);
61+
}, [setDynamicMdContent]);
6962

7063
const [isFormVisible, setIsFormVisible] = useState(false);
7164

app/sidebar.tsx

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,31 @@ import { splitMarkdown } from "./[docs_id]/splitMarkdown";
66
import { pagesList } from "./pagesList";
77
import { AccountMenu } from "./accountMenu";
88
import { ThemeToggle } from "./[docs_id]/themeToggle";
9+
import { useDynamicMdContextOptional } from "./[docs_id]/dynamicMdContext";
910

1011
const fetcher: Fetcher<string, string> = (url) =>
1112
fetch(url).then((r) => r.text());
1213

1314
export function Sidebar() {
1415
const pathname = usePathname();
1516
const docs_id = pathname.replace(/^\//, "");
16-
const { data, error, isLoading } = useSWR(`/docs/${docs_id}.md`, fetcher);
17+
const dynamicMdContextValue = useDynamicMdContextOptional();
18+
19+
// コンテキストが利用可能な場合はそれを使用し、そうでない場合はフェッチする
20+
const { data, error, isLoading } = useSWR(
21+
dynamicMdContextValue ? null : `/docs/${docs_id}.md`,
22+
fetcher
23+
);
1724

1825
if (error) console.error("Sidebar fetch error:", error);
1926

20-
const splitmdcontent = splitMarkdown(data ?? "");
27+
// コンテキストがある場合はそれを使用、ない場合はフェッチしたデータを使用
28+
const splitmdcontent = dynamicMdContextValue?.dynamicMdContent ?? splitMarkdown(data ?? "");
29+
30+
// 現在表示中のセクション(最初にinViewがtrueのもの)を見つける
31+
const currentSectionIndex = dynamicMdContextValue?.dynamicMdContent.findIndex(
32+
(section) => section.inView
33+
) ?? -1;
2134
return (
2235
<div className="bg-base-200 h-full w-80 overflow-y-auto">
2336
{/* todo: 背景色ほんとにこれでいい? */}
@@ -67,14 +80,23 @@ export function Sidebar() {
6780
</Link>
6881
{`${group.id}-${page.id}` === docs_id && !isLoading && (
6982
<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-
))}
83+
{splitmdcontent.slice(1).map((section, idx) => {
84+
// idx + 1 は実際のsectionIndexに対応(slice(1)で最初を除外しているため)
85+
const isCurrentSection = idx + 1 === currentSectionIndex;
86+
return (
87+
<li
88+
key={idx}
89+
style={{ marginLeft: section.level - 2 + "em" }}
90+
>
91+
<Link
92+
href={`#${idx + 1}`}
93+
className={isCurrentSection ? "font-bold" : ""}
94+
>
95+
{section.title}
96+
</Link>
97+
</li>
98+
);
99+
})}
78100
</ul>
79101
)}
80102
</li>

0 commit comments

Comments
 (0)