"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Typography } from "poyraz-ui/atoms"; type TocHeading = { id: string; text: string; level: 2 | 3; }; function parseHeadings(markdown: string): TocHeading[] { const headings: TocHeading[] = []; const lines = markdown.split("\n"); for (const line of lines) { const match = /^(#{2,3})\s+(.+)$/.exec(line.trim()); if (!match) continue; const level = match[1].length as 2 | 3; const raw = match[2].replace(/\*\*/g, "").replace(/\*/g, "").trim(); const id = raw .toLowerCase() .replace(/[^a-zçğıöşü0-9\s-]/g, "") .replace(/\s+/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, ""); if (id && raw) { headings.push({ id, text: raw, level }); } } return headings; } type BlogTocProps = { markdown: string; scrollerRef: React.RefObject; }; export function BlogToc({ markdown, scrollerRef }: BlogTocProps) { const headings = useMemo(() => parseHeadings(markdown), [markdown]); const [activeId, setActiveId] = useState(""); const observerRef = useRef(null); const handleClick = useCallback( (id: string) => { const scroller = scrollerRef.current; if (!scroller) return; const target = scroller.querySelector(`#${CSS.escape(id)}`); if (!target) return; target.scrollIntoView({ behavior: "smooth", block: "start" }); }, [scrollerRef], ); useEffect(() => { const scroller = scrollerRef.current; if (!scroller || headings.length === 0) return; observerRef.current = new IntersectionObserver( (entries) => { for (const entry of entries) { if (entry.isIntersecting) { setActiveId(entry.target.id); } } }, { root: scroller, rootMargin: "0px 0px -60% 0px", threshold: 0.1 }, ); const elements = headings .map((h) => scroller.querySelector(`#${CSS.escape(h.id)}`)) .filter(Boolean) as Element[]; for (const el of elements) { observerRef.current.observe(el); } return () => { observerRef.current?.disconnect(); }; }, [headings, scrollerRef]); if (headings.length < 2) return null; return ( ); }