"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; onNavigate?: () => void; }; export function BlogToc({ markdown, onNavigate }: BlogTocProps) { const headings = useMemo(() => parseHeadings(markdown), [markdown]); const [activeId, setActiveId] = useState(""); const rafRef = useRef(0); const handleClick = useCallback((id: string) => { const target = document.getElementById(id); if (!target) return; target.scrollIntoView({ behavior: "smooth", block: "start" }); setActiveId(id); onNavigate?.(); }, [onNavigate]); useEffect(() => { if (headings.length === 0) return; const OFFSET = 120; const updateActive = () => { let current = ""; for (const heading of headings) { const el = document.getElementById(heading.id); if (!el) continue; const top = el.getBoundingClientRect().top; if (top <= OFFSET) { current = heading.id; } } if (current) { setActiveId(current); } }; const onScroll = () => { cancelAnimationFrame(rafRef.current); rafRef.current = requestAnimationFrame(updateActive); }; window.addEventListener("scroll", onScroll, { passive: true }); const timer = setTimeout(updateActive, 300); return () => { window.removeEventListener("scroll", onScroll); cancelAnimationFrame(rafRef.current); clearTimeout(timer); }; }, [headings]); if (headings.length < 2) return null; return ( ); }