"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { ChevronDown, List } from "lucide-react"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { ScrollArea } from "@/components/ui/scroll-area"; import { cn } from "@/lib/utils"; import type { TocItem } from "@/lib/blog-utils"; function useScrollProgress() { const [progress, setProgress] = useState(0); const update = useCallback(() => { const { scrollY } = window; const docHeight = document.documentElement.scrollHeight - window.innerHeight; if (docHeight <= 0) { setProgress(0); return; } const value = Math.min(100, Math.max(0, (scrollY / docHeight) * 100)); setProgress(value); }, []); useEffect(() => { update(); window.addEventListener("scroll", update, { passive: true }); window.addEventListener("resize", update); return () => { window.removeEventListener("scroll", update); window.removeEventListener("resize", update); }; }, [update]); return progress; } interface TocGroup { heading: TocItem; children: TocItem[]; } function groupByLevel2(items: TocItem[]): TocGroup[] { const groups: TocGroup[] = []; let current: TocGroup | null = null; for (const item of items) { if (item.level === 2) { current = { heading: item, children: [] }; groups.push(current); } else if (current) { current.children.push(item); } } return groups; } interface BlogTocProps { items: TocItem[]; className?: string; } const tocLinkClass = ( level: number, isActive: boolean ) => cn( "block rounded-md border-r-2 rounded-s-none border-transparent py-1.5 text-sm transition-colors hover:text-foreground hover:bg-muted/60", level === 2 && "pr-3", level === 3 && "pr-5", level === 4 && "pr-7", isActive ? "font-medium text-foreground border-primary bg-muted/50" : "text-muted-foreground" ); export function BlogToc({ items, className }: BlogTocProps) { const [activeId, setActiveId] = useState(items[0]?.id ?? null); const [mobileOpen, setMobileOpen] = useState(false); const [openSections, setOpenSections] = useState>(() => new Set()); const scrollProgress = useScrollProgress(); const groups = useMemo(() => groupByLevel2(items), [items]); useEffect(() => { setOpenSections(new Set(groups.map((g) => g.heading.id))); }, [groups]); useEffect(() => { if (items.length === 0) return; const observer = new IntersectionObserver( (entries) => { for (const entry of entries) { if (entry.isIntersecting) { const id = entry.target.getAttribute("id"); if (id) setActiveId(id); } } }, { rootMargin: "-80px 0% -70% 0%", threshold: 0 } ); items.forEach(({ id }) => { const el = document.getElementById(id); if (el) observer.observe(el); }); return () => observer.disconnect(); }, [items]); useEffect(() => { if (activeId && groups.length > 0) { const group = groups.find( (g) => g.heading.id === activeId || g.children.some((c) => c.id === activeId) ); if (group) { setOpenSections((prev) => new Set(prev).add(group.heading.id)); } } }, [activeId, groups]); if (items.length === 0) return null; const renderTocLink = (item: TocItem, onNav?: () => void) => ( { setMobileOpen(false); onNav?.(); }} className={tocLinkClass(item.level, activeId === item.id)} > {item.text} ); const tocList = ( ); return ( <> {/* Desktop: sticky sidebar with scroll progress bar */} {/* Mobile: collapsible with horizontal scroll progress bar */} {/* Horizontal scroll progress indicator (mobile) */}
فهرست مطالب
{tocList}
); }