Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | 'use client'; /** * API Documentation Sidebar * Navigation tree for browsing API categories and endpoints */ import React, { useState, useEffect, useRef, useMemo } from 'react'; import { ApiCategory, ApiEndpoint } from '@/types/api-docs'; import { getMethodColor } from '@/lib/api-docs/executor'; import { Icon } from '@/components/ui/icons'; interface ApiDocsSidebarProps { categories: ApiCategory[]; selectedCategory: string | null; selectedEndpoint: ApiEndpoint | null; onSelectCategory: (categoryId: string | null) => void; onSelectEndpoint: (endpoint: ApiEndpoint) => void; } export default function ApiDocsSidebar({ categories, selectedCategory, selectedEndpoint, onSelectCategory, onSelectEndpoint}: ApiDocsSidebarProps) { // Track manually toggled categories const [manuallyExpanded, setManuallyExpanded] = useState<Set<string>>( new Set(selectedCategory ? [selectedCategory] : []) ); const selectedEndpointRef = useRef<HTMLButtonElement>(null); const categoryRefs = useRef<Map<string, HTMLDivElement>>(new Map()); // Derive effective expanded categories: manual toggles + selectedCategory const expandedCategories = useMemo(() => { const expanded = new Set(manuallyExpanded); if (selectedCategory) { expanded.add(selectedCategory); } return expanded; }, [manuallyExpanded, selectedCategory]); // Scroll category into view when selected (DOM side effect is allowed) useEffect(() => { if (selectedCategory) { const timer = setTimeout(() => { const categoryEl = categoryRefs.current.get(selectedCategory); if (categoryEl) { categoryEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } }, 50); return () => clearTimeout(timer); } }, [selectedCategory]); // Scroll selected endpoint into view after category scrolls useEffect(() => { if (selectedEndpointRef.current) { const timer = setTimeout(() => { selectedEndpointRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }, 150); return () => clearTimeout(timer); } }, [selectedEndpoint?.id]); const toggleCategory = (categoryId: string) => { setManuallyExpanded((prev) => { const next = new Set(prev); if (next.has(categoryId)) { next.delete(categoryId); } else { next.add(categoryId); } return next; }); onSelectCategory(categoryId); }; return ( <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden"> <div className="p-4 border-b border-gray-200 dark:border-gray-700"> <h2 className="font-semibold text-gray-900 dark:text-gray-100">Categories</h2> </div> <nav className="max-h-[calc(100vh-250px)] overflow-y-auto"> {categories.map((category) => ( <div key={category.id} ref={(el) => { if (el) categoryRefs.current.set(category.id, el); }} > {/* Category Header */} <button onClick={() => toggleCategory(category.id)} className={`w-full flex items-center justify-between px-4 py-3 text-left hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors ${ selectedCategory === category.id ? 'bg-indigo-50 dark:bg-indigo-900/30' : '' }`} > <div className="flex items-center gap-2"> <Icon name="chevron-right" size={16} className={`text-gray-400 dark:text-gray-500 transition-transform ${ expandedCategories.has(category.id) ? 'rotate-90' : '' }`} /> <span className="font-medium text-gray-900 dark:text-gray-100">{category.name}</span> </div> <span className="text-xs text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded"> {category.endpoints.length} </span> </button> {/* Endpoints List */} {expandedCategories.has(category.id) && ( <div className="border-t border-gray-100 dark:border-gray-700"> {category.endpoints.map((endpoint) => ( <button key={endpoint.id} ref={selectedEndpoint?.id === endpoint.id ? selectedEndpointRef : null} onClick={() => onSelectEndpoint(endpoint)} className={`w-full flex items-center gap-2 px-4 py-2 pl-8 text-left hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors ${ selectedEndpoint?.id === endpoint.id ? 'bg-indigo-100 dark:bg-indigo-900/40 border-l-2 border-indigo-500' : '' }`} > <span className={`text-xs font-mono font-semibold px-1.5 py-0.5 rounded w-14 text-center inline-block ${getMethodColor( endpoint.method )}`} > {endpoint.method} </span> <span className="text-sm text-gray-700 dark:text-gray-300 truncate flex-1"> {endpoint.summary} </span> {endpoint.adminOnly && ( <span title="Admin only"> <Icon name="lock-closed" size={12} className="text-amber-500 dark:text-amber-400" /> </span> )} </button> ))} </div> )} </div> ))} </nav> </div> ); } |