diff --git a/app/[docs_id]/dynamicMdContext.tsx b/app/[docs_id]/dynamicMdContext.tsx new file mode 100644 index 0000000..779ff4e --- /dev/null +++ b/app/[docs_id]/dynamicMdContext.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { createContext, ReactNode, useContext, useState } from "react"; +import { DynamicMarkdownSection } from "./pageContent"; + +export interface ISidebarMdContext { + sidebarMdContent: DynamicMarkdownSection[]; + setSidebarMdContent: React.Dispatch>; +} + +const SidebarMdContext = createContext(null); + +export function useSidebarMdContext() { + const context = useContext(SidebarMdContext); + if (!context) { + throw new Error( + "useSidebarMdContext must be used within a SidebarMdProvider" + ); + } + return context; +} + +export function useSidebarMdContextOptional() { + return useContext(SidebarMdContext); +} + +export function SidebarMdProvider({ + children, +}: { + children: ReactNode; +}) { + const [sidebarMdContent, setSidebarMdContent] = + useState([]); + + return ( + + {children} + + ); +} diff --git a/app/[docs_id]/pageContent.tsx b/app/[docs_id]/pageContent.tsx index 56cc6f6..418a518 100644 --- a/app/[docs_id]/pageContent.tsx +++ b/app/[docs_id]/pageContent.tsx @@ -5,6 +5,7 @@ import { MarkdownSection } from "./splitMarkdown"; import { ChatForm } from "./chatForm"; import { Heading, StyledMarkdown } from "./markdown"; import { useChatHistoryContext } from "./chatHistory"; +import { useSidebarMdContext } from "./dynamicMdContext"; import clsx from "clsx"; // MarkdownSectionに追加で、ユーザーが今そのセクションを読んでいるかどうか、などの動的な情報を持たせる @@ -19,26 +20,34 @@ interface PageContentProps { docs_id: string; } export function PageContent(props: PageContentProps) { + const { setSidebarMdContent } = useSidebarMdContext(); + + // SSR用のローカルstate const [dynamicMdContent, setDynamicMdContent] = useState< DynamicMarkdownSection[] >( - // useEffectで更新するのとは別に、SSRのための初期値 props.splitMdContent.map((section, i) => ({ ...section, inView: false, sectionId: `${props.docs_id}-${i}`, })) ); + useEffect(() => { - // props.splitMdContentが変わったときにdynamicMdContentを更新 - setDynamicMdContent( - props.splitMdContent.map((section, i) => ({ - ...section, - inView: false, - sectionId: `${props.docs_id}-${i}`, - })) - ); - }, [props.splitMdContent, props.docs_id]); + // props.splitMdContentが変わったときにローカルstateとcontextの両方を更新 + const newContent = props.splitMdContent.map((section, i) => ({ + ...section, + inView: false, + sectionId: `${props.docs_id}-${i}`, + })); + setDynamicMdContent(newContent); + setSidebarMdContent(newContent); + + // クリーンアップ:コンポーネントがアンマウントされたらcontextをクリア + return () => { + setSidebarMdContent([]); + }; + }, [props.splitMdContent, props.docs_id, setSidebarMdContent]); const sectionRefs = useRef>([]); // sectionRefsの長さをsplitMdContentに合わせる @@ -48,7 +57,7 @@ export function PageContent(props: PageContentProps) { useEffect(() => { const handleScroll = () => { - setDynamicMdContent((prevDynamicMdContent) => { + const updateContent = (prevDynamicMdContent: DynamicMarkdownSection[]) => { const dynMdContent = prevDynamicMdContent.slice(); // Reactの変更検知のために新しい配列を作成 for (let i = 0; i < sectionRefs.current.length; i++) { if (sectionRefs.current.at(i) && dynMdContent.at(i)) { @@ -58,14 +67,18 @@ export function PageContent(props: PageContentProps) { } } return dynMdContent; - }); + }; + + // ローカルstateとcontextの両方を更新 + setDynamicMdContent(updateContent); + setSidebarMdContent(updateContent); }; window.addEventListener("scroll", handleScroll); handleScroll(); return () => { window.removeEventListener("scroll", handleScroll); }; - }, []); + }, [setSidebarMdContent]); const [isFormVisible, setIsFormVisible] = useState(false); diff --git a/app/layout.tsx b/app/layout.tsx index 2589c54..59d2049 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -9,6 +9,7 @@ import { PyodideProvider } from "./terminal/python/pyodide"; import { WandboxProvider } from "./terminal/wandbox/wandbox"; import { EmbedContextProvider } from "./terminal/embedContext"; import { AutoAnonymousLogin } from "./accountMenu"; +import { SidebarMdProvider } from "./[docs_id]/dynamicMdContext"; export const metadata: Metadata = { title: "Create Next App", @@ -22,25 +23,27 @@ export default function RootLayout({ -
- -
- - - - {children} - - + +
+ +
+ + + + {children} + + +
+
+
-
-
-
+ ); diff --git a/app/sidebar.tsx b/app/sidebar.tsx index 1afa75b..b95fda3 100644 --- a/app/sidebar.tsx +++ b/app/sidebar.tsx @@ -1,23 +1,20 @@ "use client"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import useSWR, { Fetcher } from "swr"; -import { splitMarkdown } from "./[docs_id]/splitMarkdown"; import { pagesList } from "./pagesList"; import { AccountMenu } from "./accountMenu"; import { ThemeToggle } from "./[docs_id]/themeToggle"; - -const fetcher: Fetcher = (url) => - fetch(url).then((r) => r.text()); +import { useSidebarMdContext } from "./[docs_id]/dynamicMdContext"; export function Sidebar() { const pathname = usePathname(); const docs_id = pathname.replace(/^\//, ""); - const { data, error, isLoading } = useSWR(`/docs/${docs_id}.md`, fetcher); - - if (error) console.error("Sidebar fetch error:", error); - - const splitmdcontent = splitMarkdown(data ?? ""); + const { sidebarMdContent } = useSidebarMdContext(); + + // 現在表示中のセクション(最初にinViewがtrueのもの)を見つける + const currentSectionIndex = sidebarMdContent.findIndex( + (section) => section.inView + ); return (
{/* todo: 背景色ほんとにこれでいい? */} @@ -65,16 +62,25 @@ export function Sidebar() { {page.id}. {page.title} - {`${group.id}-${page.id}` === docs_id && !isLoading && ( + {`${group.id}-${page.id}` === docs_id && sidebarMdContent.length > 0 && (
    - {splitmdcontent.slice(1).map((section, idx) => ( -
  • - {section.title} -
  • - ))} + {sidebarMdContent.slice(1).map((section, idx) => { + // idx + 1 は実際のsectionIndexに対応(slice(1)で最初を除外しているため) + const isCurrentSection = idx + 1 === currentSectionIndex; + return ( +
  • + + {section.title} + +
  • + ); + })}
)}