zero-doc 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-enricher.d.ts +15 -0
- package/dist/ai-enricher.js +103 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +312 -0
- package/dist/gemini-enricher.d.ts +14 -0
- package/dist/gemini-enricher.js +96 -0
- package/dist/generator.d.ts +22 -0
- package/dist/generator.js +113 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +26 -0
- package/dist/parser.d.ts +14 -0
- package/dist/parser.js +176 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.js +2 -0
- package/package.json +57 -0
- package/scripts/copy-viewer.js +30 -0
- package/viewer/README.md +41 -0
- package/viewer/index.html +14 -0
- package/viewer/package.json +26 -0
- package/viewer/postcss.config.js +6 -0
- package/viewer/public/api-inventory.json +151 -0
- package/viewer/src/App.tsx +79 -0
- package/viewer/src/components/CodeSnippet.tsx +136 -0
- package/viewer/src/components/EndpointView.tsx +106 -0
- package/viewer/src/components/ParameterTable.tsx +71 -0
- package/viewer/src/components/Sidebar.tsx +244 -0
- package/viewer/src/contexts/ThemeContext.tsx +62 -0
- package/viewer/src/index.css +32 -0
- package/viewer/src/main.tsx +11 -0
- package/viewer/src/types.ts +54 -0
- package/viewer/tailwind.config.js +13 -0
- package/viewer/tsconfig.json +22 -0
- package/viewer/tsconfig.node.json +11 -0
- package/viewer/vite.config.ts +11 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { APIInventory, RouteEndpoint } from './types';
|
|
3
|
+
import { ThemeProvider } from './contexts/ThemeContext';
|
|
4
|
+
import Sidebar from './components/Sidebar';
|
|
5
|
+
import EndpointView from './components/EndpointView';
|
|
6
|
+
|
|
7
|
+
function App() {
|
|
8
|
+
const [inventory, setInventory] = useState<APIInventory | null>(null);
|
|
9
|
+
const [selectedEndpoint, setSelectedEndpoint] = useState<RouteEndpoint | null>(null);
|
|
10
|
+
const [loading, setLoading] = useState(true);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
fetch('/api-inventory.json')
|
|
14
|
+
.then(res => res.json())
|
|
15
|
+
.then((data: APIInventory) => {
|
|
16
|
+
setInventory(data);
|
|
17
|
+
if (data.endpoints.length > 0) {
|
|
18
|
+
setSelectedEndpoint(data.endpoints[0]);
|
|
19
|
+
}
|
|
20
|
+
setLoading(false);
|
|
21
|
+
})
|
|
22
|
+
.catch(err => {
|
|
23
|
+
console.error('Failed to load API inventory:', err);
|
|
24
|
+
setLoading(false);
|
|
25
|
+
});
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
if (loading) {
|
|
29
|
+
return (
|
|
30
|
+
<ThemeProvider>
|
|
31
|
+
<div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-gray-900">
|
|
32
|
+
<div className="text-center">
|
|
33
|
+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 dark:border-blue-400 mx-auto mb-4"></div>
|
|
34
|
+
<p className="text-gray-600 dark:text-gray-400">Loading documentation...</p>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</ThemeProvider>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!inventory) {
|
|
42
|
+
return (
|
|
43
|
+
<ThemeProvider>
|
|
44
|
+
<div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-gray-900">
|
|
45
|
+
<div className="text-center">
|
|
46
|
+
<p className="text-red-600 dark:text-red-400 mb-2">Failed to load API inventory</p>
|
|
47
|
+
<p className="text-gray-600 dark:text-gray-400 text-sm">Make sure api-inventory.json exists in the public folder</p>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</ThemeProvider>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<ThemeProvider>
|
|
56
|
+
<div className="flex h-screen bg-gray-50 dark:bg-gray-900">
|
|
57
|
+
<Sidebar
|
|
58
|
+
endpoints={inventory.endpoints}
|
|
59
|
+
selectedId={selectedEndpoint?.id}
|
|
60
|
+
onSelect={setSelectedEndpoint}
|
|
61
|
+
/>
|
|
62
|
+
<div className="flex-1 flex overflow-hidden">
|
|
63
|
+
<div className="flex-1 overflow-y-auto">
|
|
64
|
+
{selectedEndpoint ? (
|
|
65
|
+
<EndpointView endpoint={selectedEndpoint} />
|
|
66
|
+
) : (
|
|
67
|
+
<div className="p-12 text-center text-gray-500 dark:text-gray-400">
|
|
68
|
+
Select an endpoint from the sidebar
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</ThemeProvider>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default App;
|
|
79
|
+
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { RouteEndpoint } from '../types';
|
|
3
|
+
|
|
4
|
+
interface CodeSnippetProps {
|
|
5
|
+
endpoint: RouteEndpoint;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const CodeSnippet: React.FC<CodeSnippetProps> = ({ endpoint }) => {
|
|
9
|
+
const [showToast, setShowToast] = useState(false);
|
|
10
|
+
|
|
11
|
+
const pathParams = endpoint.parameters.filter(p => p.in === 'path');
|
|
12
|
+
const queryParams = endpoint.parameters.filter(p => p.in === 'query');
|
|
13
|
+
const bodyParams = endpoint.parameters.filter(p => p.in === 'body');
|
|
14
|
+
|
|
15
|
+
// Build URL with path params
|
|
16
|
+
let url = endpoint.path;
|
|
17
|
+
pathParams.forEach(param => {
|
|
18
|
+
url = url.replace(`:${param.name}`, `{${param.name}}`);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Build query string
|
|
22
|
+
const queryString = queryParams.length > 0
|
|
23
|
+
? '?' + queryParams.map(p => `${p.name}={${p.name}}`).join('&')
|
|
24
|
+
: '';
|
|
25
|
+
|
|
26
|
+
// Build cURL command
|
|
27
|
+
const baseUrl = `https://api.example.com${url}${queryString}`;
|
|
28
|
+
let curlCommand = `curl -X ${endpoint.method} \\\n '${baseUrl}'`;
|
|
29
|
+
|
|
30
|
+
if (bodyParams.length > 0) {
|
|
31
|
+
const bodyJson = bodyParams.reduce((acc, param, index) => {
|
|
32
|
+
const comma = index < bodyParams.length - 1 ? ',' : '';
|
|
33
|
+
const value = param.inferredType === 'number' ? '0' :
|
|
34
|
+
param.inferredType === 'boolean' ? 'true' :
|
|
35
|
+
'value';
|
|
36
|
+
return acc + ` "${param.name}": "${value}"${comma}\n`;
|
|
37
|
+
}, '');
|
|
38
|
+
|
|
39
|
+
curlCommand += ` \\\n -H 'Content-Type: application/json' \\\n -d '{\n${bodyJson} }'`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const copyToClipboard = async (curlCommand: string) => {
|
|
43
|
+
try {
|
|
44
|
+
await navigator.clipboard.writeText(curlCommand);
|
|
45
|
+
setShowToast(true);
|
|
46
|
+
setTimeout(() => setShowToast(false), 2000);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error('Failed to copy:', err);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<>
|
|
54
|
+
<div className="bg-gray-900 rounded-lg border border-gray-700 overflow-hidden shadow-lg">
|
|
55
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700 bg-gray-800/50">
|
|
56
|
+
<div className="flex items-center gap-2">
|
|
57
|
+
<div className="flex gap-1.5">
|
|
58
|
+
<div className="w-3 h-3 rounded-full bg-red-500"></div>
|
|
59
|
+
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
|
|
60
|
+
<div className="w-3 h-3 rounded-full bg-green-500"></div>
|
|
61
|
+
</div>
|
|
62
|
+
<span className="text-xs font-medium text-gray-400 uppercase tracking-wider">curl</span>
|
|
63
|
+
</div>
|
|
64
|
+
<button
|
|
65
|
+
onClick={() => copyToClipboard(curlCommand)}
|
|
66
|
+
className="px-3 py-1.5 text-xs text-gray-400 hover:text-white transition-colors rounded hover:bg-gray-700/50 border border-gray-700 hover:border-gray-600"
|
|
67
|
+
title="Copy to clipboard"
|
|
68
|
+
>
|
|
69
|
+
📋 Copy
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="p-5 overflow-x-auto bg-gray-950">
|
|
73
|
+
<pre className="text-sm text-gray-100 font-mono leading-relaxed whitespace-pre-wrap">
|
|
74
|
+
<code className="block">
|
|
75
|
+
<span className="text-green-400 select-none">$ </span>
|
|
76
|
+
<span className="text-blue-400">curl</span>
|
|
77
|
+
<span className="text-gray-300"> -X </span>
|
|
78
|
+
<span className="text-yellow-400 font-semibold">{endpoint.method}</span>
|
|
79
|
+
<span className="text-gray-300"> \</span>
|
|
80
|
+
<br />
|
|
81
|
+
<span className="text-gray-500 ml-4"> </span>
|
|
82
|
+
<span className="text-purple-300">'{baseUrl}'</span>
|
|
83
|
+
{bodyParams.length > 0 && (
|
|
84
|
+
<>
|
|
85
|
+
<span className="text-gray-300"> \</span>
|
|
86
|
+
<br />
|
|
87
|
+
<span className="text-gray-500 ml-4"> </span>
|
|
88
|
+
<span className="text-blue-400">-H</span>
|
|
89
|
+
<span className="text-gray-300"> </span>
|
|
90
|
+
<span className="text-purple-300">'Content-Type: application/json'</span>
|
|
91
|
+
<span className="text-gray-300"> \</span>
|
|
92
|
+
<br />
|
|
93
|
+
<span className="text-gray-500 ml-4"> </span>
|
|
94
|
+
<span className="text-blue-400">-d</span>
|
|
95
|
+
<span className="text-gray-300"> </span>
|
|
96
|
+
<span className="text-purple-300">'{'{'}</span>
|
|
97
|
+
<br />
|
|
98
|
+
{bodyParams.map((param, index) => {
|
|
99
|
+
const value = param.inferredType === 'number' ? '0' :
|
|
100
|
+
param.inferredType === 'boolean' ? 'true' :
|
|
101
|
+
'value';
|
|
102
|
+
const comma = index < bodyParams.length - 1 ? ',' : '';
|
|
103
|
+
return (
|
|
104
|
+
<React.Fragment key={param.name}>
|
|
105
|
+
<span className="text-gray-500 ml-8"> </span>
|
|
106
|
+
<span className="text-yellow-300">"{param.name}"</span>
|
|
107
|
+
<span className="text-gray-300">: </span>
|
|
108
|
+
<span className="text-green-300">"{value}"</span>
|
|
109
|
+
<span className="text-gray-300">{comma}</span>
|
|
110
|
+
<br />
|
|
111
|
+
</React.Fragment>
|
|
112
|
+
);
|
|
113
|
+
})}
|
|
114
|
+
<span className="text-gray-500 ml-8"> </span>
|
|
115
|
+
<span className="text-purple-300">{'}'}'</span>
|
|
116
|
+
</>
|
|
117
|
+
)}
|
|
118
|
+
</code>
|
|
119
|
+
</pre>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Toast Notification */}
|
|
124
|
+
{showToast && (
|
|
125
|
+
<div className="fixed bottom-4 right-4 bg-green-600 text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-2 z-50 animate-fade-in">
|
|
126
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
127
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
128
|
+
</svg>
|
|
129
|
+
<span className="text-sm font-medium">Copied to clipboard!</span>
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
</>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export default CodeSnippet;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { RouteEndpoint } from '../types';
|
|
3
|
+
import ParameterTable from './ParameterTable';
|
|
4
|
+
import CodeSnippet from './CodeSnippet';
|
|
5
|
+
|
|
6
|
+
interface EndpointViewProps {
|
|
7
|
+
endpoint: RouteEndpoint;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const EndpointView: React.FC<EndpointViewProps> = ({ endpoint }) => {
|
|
11
|
+
const getMethodColor = (method: string) => {
|
|
12
|
+
const colors: Record<string, string> = {
|
|
13
|
+
GET: 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 border-green-300 dark:border-green-700',
|
|
14
|
+
POST: 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 border-blue-300 dark:border-blue-700',
|
|
15
|
+
PUT: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300 border-yellow-300 dark:border-yellow-700',
|
|
16
|
+
PATCH: 'bg-orange-100 dark:bg-orange-900/30 text-orange-800 dark:text-orange-300 border-orange-300 dark:border-orange-700',
|
|
17
|
+
DELETE: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300 border-red-300 dark:border-red-700',
|
|
18
|
+
};
|
|
19
|
+
return colors[method] || 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-300 border-gray-300 dark:border-gray-700';
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const pathParams = endpoint.parameters.filter(p => p.in === 'path');
|
|
23
|
+
const queryParams = endpoint.parameters.filter(p => p.in === 'query');
|
|
24
|
+
const bodyParams = endpoint.parameters.filter(p => p.in === 'body');
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="flex-1 overflow-y-auto bg-gray-50 dark:bg-gray-900">
|
|
28
|
+
<div className="max-w-7xl mx-auto px-8 py-8">
|
|
29
|
+
{/* Header */}
|
|
30
|
+
<div className="mb-8">
|
|
31
|
+
<div className="flex items-center gap-3 mb-4">
|
|
32
|
+
<span className={`px-3 py-1 rounded font-semibold text-sm border ${getMethodColor(endpoint.method)}`}>
|
|
33
|
+
{endpoint.method}
|
|
34
|
+
</span>
|
|
35
|
+
<code className="text-2xl font-mono text-gray-900 dark:text-white">{endpoint.path}</code>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{endpoint.metadata?.title && (
|
|
39
|
+
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-3">
|
|
40
|
+
{endpoint.metadata.title}
|
|
41
|
+
</h1>
|
|
42
|
+
)}
|
|
43
|
+
|
|
44
|
+
{endpoint.metadata?.description && (
|
|
45
|
+
<p className="text-gray-600 dark:text-gray-300 text-lg leading-relaxed">
|
|
46
|
+
{endpoint.metadata.description}
|
|
47
|
+
</p>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
52
|
+
{/* Main Content */}
|
|
53
|
+
<div className="lg:col-span-2 space-y-8">
|
|
54
|
+
{/* Path Parameters */}
|
|
55
|
+
{pathParams.length > 0 && (
|
|
56
|
+
<section>
|
|
57
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Path Parameters</h2>
|
|
58
|
+
<ParameterTable parameters={pathParams} />
|
|
59
|
+
</section>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
{/* Query Parameters */}
|
|
63
|
+
{queryParams.length > 0 && (
|
|
64
|
+
<section>
|
|
65
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Query Parameters</h2>
|
|
66
|
+
<ParameterTable parameters={queryParams} />
|
|
67
|
+
</section>
|
|
68
|
+
)}
|
|
69
|
+
|
|
70
|
+
{/* Request Body */}
|
|
71
|
+
{bodyParams.length > 0 && (
|
|
72
|
+
<section>
|
|
73
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Request Body</h2>
|
|
74
|
+
<ParameterTable parameters={bodyParams} />
|
|
75
|
+
</section>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
{/* Response */}
|
|
79
|
+
<section>
|
|
80
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Response</h2>
|
|
81
|
+
<div className="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
|
82
|
+
<p className="text-gray-600 dark:text-gray-300">
|
|
83
|
+
{endpoint.method === 'GET' ? 'Returns the requested resource.' :
|
|
84
|
+
endpoint.method === 'POST' ? 'Returns the created resource.' :
|
|
85
|
+
endpoint.method === 'PUT' ? 'Returns the updated resource.' :
|
|
86
|
+
endpoint.method === 'DELETE' ? 'Returns 204 No Content on success.' :
|
|
87
|
+
'Returns the result of the operation.'}
|
|
88
|
+
</p>
|
|
89
|
+
</div>
|
|
90
|
+
</section>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Code Snippet Sidebar */}
|
|
94
|
+
<div className="lg:col-span-1">
|
|
95
|
+
<div className="sticky top-8">
|
|
96
|
+
<CodeSnippet endpoint={endpoint} />
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export default EndpointView;
|
|
106
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { RouteParameter } from '../types';
|
|
3
|
+
|
|
4
|
+
interface ParameterTableProps {
|
|
5
|
+
parameters: RouteParameter[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const ParameterTable: React.FC<ParameterTableProps> = ({ parameters }) => {
|
|
9
|
+
const getTypeColor = (type?: string) => {
|
|
10
|
+
const colors: Record<string, string> = {
|
|
11
|
+
string: 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300',
|
|
12
|
+
number: 'bg-green-50 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
|
13
|
+
boolean: 'bg-purple-50 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300',
|
|
14
|
+
object: 'bg-orange-50 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300',
|
|
15
|
+
array: 'bg-pink-50 dark:bg-pink-900/30 text-pink-700 dark:text-pink-300',
|
|
16
|
+
};
|
|
17
|
+
return colors[type || 'string'] || 'bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
|
22
|
+
<table className="w-full">
|
|
23
|
+
<thead className="bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
|
24
|
+
<tr>
|
|
25
|
+
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
|
26
|
+
Parameter
|
|
27
|
+
</th>
|
|
28
|
+
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
|
29
|
+
Type
|
|
30
|
+
</th>
|
|
31
|
+
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
|
32
|
+
Required
|
|
33
|
+
</th>
|
|
34
|
+
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
|
35
|
+
Description
|
|
36
|
+
</th>
|
|
37
|
+
</tr>
|
|
38
|
+
</thead>
|
|
39
|
+
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
|
40
|
+
{parameters.map((param, index) => (
|
|
41
|
+
<tr key={index} className="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
42
|
+
<td className="px-4 py-3">
|
|
43
|
+
<code className="text-sm font-mono text-gray-900 dark:text-white">{param.name}</code>
|
|
44
|
+
</td>
|
|
45
|
+
<td className="px-4 py-3">
|
|
46
|
+
{param.inferredType && (
|
|
47
|
+
<span className={`inline-block px-2 py-1 rounded text-xs font-medium ${getTypeColor(param.inferredType)}`}>
|
|
48
|
+
{param.inferredType}
|
|
49
|
+
</span>
|
|
50
|
+
)}
|
|
51
|
+
</td>
|
|
52
|
+
<td className="px-4 py-3">
|
|
53
|
+
{param.required ? (
|
|
54
|
+
<span className="text-xs text-red-600 dark:text-red-400 font-medium">Required</span>
|
|
55
|
+
) : (
|
|
56
|
+
<span className="text-xs text-gray-400 dark:text-gray-500">Optional</span>
|
|
57
|
+
)}
|
|
58
|
+
</td>
|
|
59
|
+
<td className="px-4 py-3 text-sm text-gray-600 dark:text-gray-300">
|
|
60
|
+
{param.description || 'No description available'}
|
|
61
|
+
</td>
|
|
62
|
+
</tr>
|
|
63
|
+
))}
|
|
64
|
+
</tbody>
|
|
65
|
+
</table>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default ParameterTable;
|
|
71
|
+
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import React, { useState, useMemo, useEffect } from 'react';
|
|
2
|
+
import { RouteEndpoint } from '../types';
|
|
3
|
+
import { useTheme } from '../contexts/ThemeContext';
|
|
4
|
+
|
|
5
|
+
interface SidebarProps {
|
|
6
|
+
endpoints: RouteEndpoint[];
|
|
7
|
+
selectedId?: string;
|
|
8
|
+
onSelect: (endpoint: RouteEndpoint) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const Sidebar: React.FC<SidebarProps> = ({ endpoints, selectedId, onSelect }) => {
|
|
12
|
+
const { isDark, toggleTheme } = useTheme();
|
|
13
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
14
|
+
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
|
15
|
+
|
|
16
|
+
// Initialize with all groups expanded
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const groups = endpoints.reduce((acc, endpoint) => {
|
|
19
|
+
const group = endpoint.group || 'Other';
|
|
20
|
+
acc.add(group);
|
|
21
|
+
return acc;
|
|
22
|
+
}, new Set<string>());
|
|
23
|
+
setExpandedGroups(groups);
|
|
24
|
+
}, [endpoints]);
|
|
25
|
+
|
|
26
|
+
// Filter endpoints based on search
|
|
27
|
+
const filteredEndpoints = useMemo(() => {
|
|
28
|
+
if (!searchQuery.trim()) return endpoints;
|
|
29
|
+
|
|
30
|
+
const query = searchQuery.toLowerCase();
|
|
31
|
+
return endpoints.filter(endpoint => {
|
|
32
|
+
// Search in title
|
|
33
|
+
if (endpoint.metadata?.title?.toLowerCase().includes(query)) return true;
|
|
34
|
+
|
|
35
|
+
// Search in group
|
|
36
|
+
if (endpoint.group?.toLowerCase().includes(query)) return true;
|
|
37
|
+
|
|
38
|
+
// Search in path/URL
|
|
39
|
+
if (endpoint.path.toLowerCase().includes(query)) return true;
|
|
40
|
+
|
|
41
|
+
// Search in description
|
|
42
|
+
if (endpoint.metadata?.description?.toLowerCase().includes(query)) return true;
|
|
43
|
+
|
|
44
|
+
// Search in method
|
|
45
|
+
if (endpoint.method.toLowerCase().includes(query)) return true;
|
|
46
|
+
|
|
47
|
+
// Search in parameters
|
|
48
|
+
if (endpoint.parameters.some(p =>
|
|
49
|
+
p.name.toLowerCase().includes(query) ||
|
|
50
|
+
p.description?.toLowerCase().includes(query)
|
|
51
|
+
)) return true;
|
|
52
|
+
|
|
53
|
+
return false;
|
|
54
|
+
});
|
|
55
|
+
}, [endpoints, searchQuery]);
|
|
56
|
+
|
|
57
|
+
const groupedByCategory = useMemo(() => {
|
|
58
|
+
return filteredEndpoints.reduce((acc, endpoint) => {
|
|
59
|
+
const group = endpoint.group || 'Other';
|
|
60
|
+
if (!acc[group]) acc[group] = {};
|
|
61
|
+
|
|
62
|
+
const method = endpoint.method;
|
|
63
|
+
if (!acc[group][method]) acc[group][method] = [];
|
|
64
|
+
acc[group][method].push(endpoint);
|
|
65
|
+
|
|
66
|
+
return acc;
|
|
67
|
+
}, {} as Record<string, Record<string, RouteEndpoint[]>>);
|
|
68
|
+
}, [filteredEndpoints]);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (searchQuery.trim()) {
|
|
72
|
+
const groups = Object.keys(groupedByCategory);
|
|
73
|
+
setExpandedGroups(new Set(groups));
|
|
74
|
+
}
|
|
75
|
+
}, [searchQuery, groupedByCategory]);
|
|
76
|
+
|
|
77
|
+
const toggleGroup = (groupName: string) => {
|
|
78
|
+
setExpandedGroups(prev => {
|
|
79
|
+
const next = new Set(prev);
|
|
80
|
+
if (next.has(groupName)) {
|
|
81
|
+
next.delete(groupName);
|
|
82
|
+
} else {
|
|
83
|
+
next.add(groupName);
|
|
84
|
+
}
|
|
85
|
+
return next;
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const getMethodColor = (method: string) => {
|
|
90
|
+
const colors: Record<string, string> = {
|
|
91
|
+
GET: 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 border-green-200 dark:border-green-700',
|
|
92
|
+
POST: 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 border-blue-200 dark:border-blue-700',
|
|
93
|
+
PUT: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300 border-yellow-200 dark:border-yellow-700',
|
|
94
|
+
PATCH: 'bg-orange-100 dark:bg-orange-900/30 text-orange-800 dark:text-orange-300 border-orange-200 dark:border-orange-700',
|
|
95
|
+
DELETE: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300 border-red-200 dark:border-red-700',
|
|
96
|
+
};
|
|
97
|
+
return colors[method] || 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-300 border-gray-200 dark:border-gray-700';
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Sort groups alphabetically
|
|
101
|
+
const sortedGroups = Object.keys(groupedByCategory).sort();
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 overflow-y-auto flex flex-col h-full">
|
|
105
|
+
{/* Header */}
|
|
106
|
+
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
|
107
|
+
<div className="flex items-center justify-between mb-2">
|
|
108
|
+
<h1 className="text-xl font-bold text-gray-900 dark:text-white">API Reference</h1>
|
|
109
|
+
<button
|
|
110
|
+
onClick={(e) => {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
e.stopPropagation();
|
|
113
|
+
toggleTheme();
|
|
114
|
+
}}
|
|
115
|
+
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
|
116
|
+
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
117
|
+
type="button"
|
|
118
|
+
>
|
|
119
|
+
{isDark ? (
|
|
120
|
+
<svg className="w-5 h-5 text-gray-600 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
121
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
122
|
+
</svg>
|
|
123
|
+
) : (
|
|
124
|
+
<svg className="w-5 h-5 text-gray-600 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
125
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
|
126
|
+
</svg>
|
|
127
|
+
)}
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
131
|
+
{filteredEndpoints.length} of {endpoints.length} endpoints
|
|
132
|
+
</p>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{/* Search Bar */}
|
|
136
|
+
<div className="p-3 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
|
137
|
+
<div className="relative">
|
|
138
|
+
<input
|
|
139
|
+
type="text"
|
|
140
|
+
placeholder="Search endpoints..."
|
|
141
|
+
value={searchQuery}
|
|
142
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
143
|
+
className="w-full px-3 py-2 pl-9 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
144
|
+
/>
|
|
145
|
+
<svg
|
|
146
|
+
className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-400 dark:text-gray-500"
|
|
147
|
+
fill="none"
|
|
148
|
+
stroke="currentColor"
|
|
149
|
+
viewBox="0 0 24 24"
|
|
150
|
+
>
|
|
151
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
152
|
+
</svg>
|
|
153
|
+
{searchQuery && (
|
|
154
|
+
<button
|
|
155
|
+
onClick={() => setSearchQuery('')}
|
|
156
|
+
className="absolute right-2.5 top-2.5 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
|
|
157
|
+
>
|
|
158
|
+
✕
|
|
159
|
+
</button>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{/* Navigation */}
|
|
165
|
+
<nav className="flex-1 overflow-y-auto p-2">
|
|
166
|
+
{sortedGroups.length === 0 ? (
|
|
167
|
+
<div className="p-4 text-center text-gray-500 dark:text-gray-400 text-sm">
|
|
168
|
+
No endpoints found
|
|
169
|
+
</div>
|
|
170
|
+
) : (
|
|
171
|
+
sortedGroups.map((groupName) => {
|
|
172
|
+
const groupEndpoints = groupedByCategory[groupName];
|
|
173
|
+
const methodKeys = Object.keys(groupEndpoints).sort();
|
|
174
|
+
const isExpanded = expandedGroups.has(groupName);
|
|
175
|
+
const totalInGroup = Object.values(groupEndpoints).flat().length;
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div key={groupName} className="mb-2">
|
|
179
|
+
{/* Collapsible Group Header */}
|
|
180
|
+
<button
|
|
181
|
+
onClick={() => toggleGroup(groupName)}
|
|
182
|
+
className="w-full flex items-center justify-between px-2 py-2 mb-1 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
|
183
|
+
>
|
|
184
|
+
<div className="text-left">
|
|
185
|
+
<h2 className="text-sm font-bold text-gray-900 dark:text-white">{groupName}</h2>
|
|
186
|
+
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
187
|
+
{totalInGroup} endpoint{totalInGroup !== 1 ? 's' : ''}
|
|
188
|
+
</p>
|
|
189
|
+
</div>
|
|
190
|
+
<svg
|
|
191
|
+
className={`h-4 w-4 text-gray-400 dark:text-gray-500 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
|
|
192
|
+
fill="none"
|
|
193
|
+
stroke="currentColor"
|
|
194
|
+
viewBox="0 0 24 24"
|
|
195
|
+
>
|
|
196
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
197
|
+
</svg>
|
|
198
|
+
</button>
|
|
199
|
+
|
|
200
|
+
{/* Collapsible Content */}
|
|
201
|
+
{isExpanded && (
|
|
202
|
+
<div className="ml-2">
|
|
203
|
+
{methodKeys.map((method) => {
|
|
204
|
+
const methodEndpoints = groupEndpoints[method];
|
|
205
|
+
return (
|
|
206
|
+
<div key={`${groupName}-${method}`} className="mb-3">
|
|
207
|
+
<div className="px-2 py-1 mb-1">
|
|
208
|
+
<span className={`text-xs font-semibold px-2 py-1 rounded border ${getMethodColor(method)}`}>
|
|
209
|
+
{method}
|
|
210
|
+
</span>
|
|
211
|
+
</div>
|
|
212
|
+
{methodEndpoints.map((endpoint) => (
|
|
213
|
+
<button
|
|
214
|
+
key={endpoint.id}
|
|
215
|
+
onClick={() => onSelect(endpoint)}
|
|
216
|
+
className={`w-full text-left px-3 py-2 mb-1 rounded text-sm transition-colors ${
|
|
217
|
+
selectedId === endpoint.id
|
|
218
|
+
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-900 dark:text-blue-300 border-l-2 border-blue-600 dark:border-blue-500'
|
|
219
|
+
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
220
|
+
}`}
|
|
221
|
+
>
|
|
222
|
+
<div className="font-mono text-xs">{endpoint.path}</div>
|
|
223
|
+
{endpoint.metadata?.title && (
|
|
224
|
+
<div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 truncate">
|
|
225
|
+
{endpoint.metadata.title}
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
</button>
|
|
229
|
+
))}
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
})}
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
})
|
|
238
|
+
)}
|
|
239
|
+
</nav>
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export default Sidebar;
|