elder_docs 0.1.2 → 0.1.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f4aabd2e66c168f676bd5eb2e2f2e3266456e5800d3e88233a0897fcdae3060
4
- data.tar.gz: 0e65cc39e7c4165aa4bb7eef14109ef0228b451cfc7e6b58ca93bd23eb38c894
3
+ metadata.gz: 20f0c58e7d2f5a6497d153f05cc9127730b8897b34a0a9ac8a42959e49f263f4
4
+ data.tar.gz: e9230ebcf9b18e2e3b2809dd2cd7d2880e6fe346e0d251ea32f3011a579a42f9
5
5
  SHA512:
6
- metadata.gz: 219d9898dbff6eaf5718b2d8750643fd902f875a690990d6d4d7b1d43a3106bb31a7c184e765cc88a3e67908ebd3056c17d3b4e0db1d142b36bfa34e81bda3c2
7
- data.tar.gz: dd17c7cdf76a23782758eb95f72ecf1f090bfad4ee509f23cabd7612cddaf1250f0b7c38cd7ec57256e1c5503a1b9de4e73056871dc101d4622ccbc895008fa9
6
+ metadata.gz: f4b30353dd92332b3a017ca6df2598daeaf96ca4c5ce67b2d0100eaeda4d26b61dd949168440fa8aa60bd3914c81fbfa4c8fed0a303b2e0bbbda16bef3b8fa03
7
+ data.tar.gz: e13f040aa72ecea558922a99c678975b324f5860d5e8bd035268b6337eee23bb4ef17d879388f7ce5edaf71fa2e01a53ec95874d9eb647e013bf31adbc3f048a
data/README.md CHANGED
@@ -145,6 +145,49 @@ bundle exec elderdocs deploy \
145
145
  - Rails >= 6.0
146
146
  - Node.js >= 16.0 (for building assets)
147
147
 
148
+ ## Development
149
+
150
+ ### Quick Development Build
151
+
152
+ For rapid iteration during development:
153
+
154
+ ```bash
155
+ ./dev_build.sh
156
+ # or
157
+ rake elderdocs:dev
158
+ ```
159
+
160
+ This rebuilds and reinstalls the gem locally without publishing.
161
+
162
+ ### Full Build and Deploy
163
+
164
+ To build, test, and optionally publish a new version:
165
+
166
+ ```bash
167
+ ./build_and_deploy.sh
168
+ # or
169
+ rake elderdocs:deploy
170
+ ```
171
+
172
+ Options:
173
+ - `--skip-publish` - Build and test but don't publish to RubyGems
174
+ - `--skip-tests` - Skip running tests
175
+
176
+ ### Manual Build
177
+
178
+ ```bash
179
+ # Build gem
180
+ rake elderdocs:build
181
+ # or
182
+ gem build elder_docs.gemspec
183
+
184
+ # Install locally
185
+ gem install elder_docs-*.gem --local
186
+
187
+ # Publish (when ready)
188
+ gem push elder_docs-*.gem
189
+ ```
190
+
148
191
  ## License
149
192
 
150
193
  MIT
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
10
+ <title>ElderDocs - API Documentation</title>
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+ <script src="/data.js"></script>
15
+ <script type="module" src="/src/main.jsx"></script>
16
+ </body>
17
+ </html>
18
+
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "elder-docs-frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^18.2.0",
13
+ "react-dom": "^18.2.0",
14
+ "react-markdown": "^9.0.0",
15
+ "react-syntax-highlighter": "^15.5.0",
16
+ "prismjs": "^1.29.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^18.2.0",
20
+ "@types/react-dom": "^18.2.0",
21
+ "@vitejs/plugin-react": "^4.0.0",
22
+ "autoprefixer": "^10.4.14",
23
+ "postcss": "^8.4.24",
24
+ "tailwindcss": "^3.3.0",
25
+ "vite": "^4.4.0"
26
+ }
27
+ }
28
+
@@ -0,0 +1,7 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
7
+
@@ -0,0 +1,2 @@
1
+ window.ElderDocsData = {"openapi":{"openapi":"3.0.0","info":{"title":"Test API - ElderDocs Demo","version":"1.0.0","description":"A comprehensive test API showcasing various endpoints, methods, and payloads for ElderDocs demonstration"},"servers":[{"url":"https://jsonplaceholder.typicode.com","description":"Test server (JSONPlaceholder)"},{"url":"https://api.example.com","description":"Production server"}],"paths":{"/posts":{"get":{"summary":"Get all posts","description":"Retrieve a list of all blog posts with optional filtering and pagination","parameters":[{"name":"userId","in":"query","description":"Filter posts by user ID","required":false,"schema":{"type":"integer"}},{"name":"_limit","in":"query","description":"Limit the number of results","required":false,"schema":{"type":"integer","default":10}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Post"}}}}}}},"post":{"summary":"Create a new post","description":"Create a new blog post with title, body, and user ID","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePostRequest"},"example":{"title":"My New Post","body":"This is the content of my new post","userId":1}}}},"responses":{"201":{"description":"Post created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Post"}}}},"400":{"description":"Bad request - invalid input"}}}},"/posts/{id}":{"get":{"summary":"Get post by ID","description":"Retrieve a specific post by its unique identifier","parameters":[{"name":"id","in":"path","required":true,"description":"Post ID","schema":{"type":"integer"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Post"}}}},"404":{"description":"Post not found"}}},"put":{"summary":"Update entire post","description":"Replace all fields of an existing post","parameters":[{"name":"id","in":"path","required":true,"description":"Post ID","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePostRequest"},"example":{"id":1,"title":"Updated Post Title","body":"Updated post content","userId":1}}}},"responses":{"200":{"description":"Post updated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Post"}}}}}},"patch":{"summary":"Partially update post","description":"Update only specified fields of a post","parameters":[{"name":"id","in":"path","required":true,"description":"Post ID","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"body":{"type":"string"}}},"example":{"title":"Partially Updated Title"}}}},"responses":{"200":{"description":"Post partially updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Post"}}}}}},"delete":{"summary":"Delete a post","description":"Permanently delete a post by ID","parameters":[{"name":"id","in":"path","required":true,"description":"Post ID","schema":{"type":"integer"}}],"responses":{"200":{"description":"Post deleted successfully"},"404":{"description":"Post not found"}}}},"/users":{"get":{"summary":"List all users","description":"Get a list of all registered users","responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}}}}}},"/users/{id}":{"get":{"summary":"Get user by ID","description":"Retrieve detailed information about a specific user","parameters":[{"name":"id","in":"path","required":true,"description":"User ID","schema":{"type":"integer"}}],"responses":{"200":{"description":"User found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"404":{"description":"User not found"}}}},"/comments":{"get":{"summary":"Get all comments","description":"Retrieve comments with optional filtering","parameters":[{"name":"postId","in":"query","description":"Filter comments by post ID","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"List of comments","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Comment"}}}}}}},"post":{"summary":"Create a comment","description":"Add a new comment to a post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCommentRequest"},"example":{"postId":1,"name":"John Doe","email":"john@example.com","body":"This is a great post!"}}}},"responses":{"201":{"description":"Comment created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Comment"}}}}}}}},"components":{"schemas":{"Post":{"type":"object","properties":{"id":{"type":"integer","description":"Unique post identifier"},"title":{"type":"string","description":"Post title"},"body":{"type":"string","description":"Post content"},"userId":{"type":"integer","description":"ID of the user who created the post"}}},"CreatePostRequest":{"type":"object","required":["title","body","userId"],"properties":{"title":{"type":"string","description":"Post title","minLength":1,"maxLength":200},"body":{"type":"string","description":"Post content"},"userId":{"type":"integer","description":"User ID"}}},"UpdatePostRequest":{"type":"object","required":["id","title","body","userId"],"properties":{"id":{"type":"integer"},"title":{"type":"string"},"body":{"type":"string"},"userId":{"type":"integer"}}},"User":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"username":{"type":"string"},"email":{"type":"string","format":"email"},"address":{"type":"object","properties":{"street":{"type":"string"},"city":{"type":"string"},"zipcode":{"type":"string"}}},"phone":{"type":"string"},"website":{"type":"string"}}},"Comment":{"type":"object","properties":{"id":{"type":"integer"},"postId":{"type":"integer"},"name":{"type":"string"},"email":{"type":"string","format":"email"},"body":{"type":"string"}}},"CreateCommentRequest":{"type":"object","required":["postId","name","email","body"],"properties":{"postId":{"type":"integer"},"name":{"type":"string"},"email":{"type":"string","format":"email"},"body":{"type":"string"}}}}}},"articles":[{"id":"getting_started","title":"🚀 Getting Started","markdown_content":"# Welcome to ElderDocs!\n\nThis is a **dead simple** API documentation platform.\n\n## Quick Start\n\n1. **Select an endpoint** from the sidebar\n2. **Fill in parameters** (if needed)\n3. **Click SEND REQUEST**\n4. **See the response** instantly\n\nThat's it! No complicated setup needed.\n\n## Features\n\n- ✅ **Interactive API Explorer** - Try endpoints right in your browser\n- ✅ **Multiple Auth Types** - Bearer tokens, API keys, Basic auth, OAuth2\n- ✅ **Beautiful Design** - Bright yellow and white with sharp corners\n- ✅ **Big Fonts** - Easy to read, easy to use\n- ✅ **Real-time Testing** - See responses immediately\n\n## Authentication\n\nChoose your authentication method from the dropdown:\n\n- **Bearer Token**: Standard OAuth2 bearer tokens\n- **API Key Header**: Custom API key headers\n- **Basic Auth**: Username:password format\n- **OAuth 2.0**: OAuth token support\n\nEnter your credentials once, and they'll be saved for all requests!"},{"id":"api_basics","title":"📖 API Basics","markdown_content":"## Understanding HTTP Methods\n\n### GET\n\nUse GET to **retrieve** data. It's safe and doesn't modify anything.\n\n```bash\nGET /posts\n```\n\n### POST\n\nUse POST to **create** new resources.\n\n```bash\nPOST /posts\nContent-Type: application/json\n\n{\n \"title\": \"My Post\",\n \"body\": \"Content here\",\n \"userId\": 1\n}\n```\n\n### PUT\n\nUse PUT to **replace** an entire resource.\n\n```bash\nPUT /posts/1\n```\n\n### PATCH\n\nUse PATCH to **partially update** a resource.\n\n```bash\nPATCH /posts/1\n```\n\n### DELETE\n\nUse DELETE to **remove** a resource.\n\n```bash\nDELETE /posts/1\n```\n\n## Response Codes\n\n- **200 OK**: Success!\n- **201 Created**: Resource created\n- **400 Bad Request**: Invalid input\n- **404 Not Found**: Resource doesn't exist\n- **500 Server Error**: Something went wrong"},{"id":"testing_tips","title":"💡 Testing Tips","markdown_content":"## Pro Tips for Testing\n\n### 1. Start Simple\n\nBegin with GET requests - they're the easiest!\n\n### 2. Check Required Fields\n\nRequired parameters are marked with a red asterisk (*).\n\n### 3. Use Examples\n\nMany endpoints have example payloads. Copy and modify them!\n\n### 4. Read the Response\n\nThe response shows:\n\n- **Status code** (200, 404, etc.)\n- **Response time** (how fast the API responded)\n- **Response body** (the actual data)\n\n### 5. Try Different Auth Methods\n\nSwitch between authentication types to see which works best for your API.\n\n### 6. Path Parameters\n\nFor endpoints like `/posts/{id}`, enter the ID in the Path Parameters section.\n\n### 7. Query Parameters\n\nAdd filters and pagination using Query Parameters.\n\n## Common Issues\n\n**CORS Error?**\n\nSome APIs block browser requests. Use a CORS proxy or test from your backend.\n\n**401 Unauthorized?**\n\nCheck your authentication credentials.\n\n**404 Not Found?**\n\nVerify the endpoint path and any path parameters."}],"api_server":"https://jsonplaceholder.typicode.com","api_servers":[],"auth_types":["bearer","api_key","basic","oauth2"],"ui_config":{},"generated_at":"2025-11-24T14:09:15+05:30"};
2
+ window.dispatchEvent(new Event('elderdocs:data_loaded'));
@@ -0,0 +1,154 @@
1
+ import React, { useState, useEffect } from 'react'
2
+ import Sidebar from './components/Sidebar'
3
+ import ContentPanel from './components/ContentPanel'
4
+ import ApiExplorer from './components/ApiExplorer'
5
+ import { ApiKeyProvider } from './contexts/ApiKeyContext'
6
+
7
+ function App() {
8
+ const [data, setData] = useState(null)
9
+ const [loading, setLoading] = useState(true)
10
+ const [activeView, setActiveView] = useState('api') // 'api' or 'articles'
11
+ const [selectedItem, setSelectedItem] = useState(null)
12
+
13
+ useEffect(() => {
14
+ if (data?.ui_config) {
15
+ const root = document.documentElement
16
+ const ui = data.ui_config
17
+
18
+ if (ui.colors) {
19
+ if (ui.colors.primary) root.style.setProperty('--bd-primary', ui.colors.primary)
20
+ if (ui.colors.secondary) {
21
+ root.style.setProperty('--bd-secondary', ui.colors.secondary)
22
+ }
23
+ if (ui.colors.background) root.style.setProperty('--bd-background', ui.colors.background)
24
+ if (ui.colors.surface) root.style.setProperty('--bd-surface', ui.colors.surface)
25
+ }
26
+
27
+ if (ui.corner_radius) {
28
+ root.style.setProperty('--bd-corner-radius', ui.corner_radius)
29
+ }
30
+
31
+ if (ui.font_heading) {
32
+ root.style.setProperty('--bd-font-heading', `'${ui.font_heading}', sans-serif`)
33
+ // Dynamically load font
34
+ const link = document.createElement('link')
35
+ link.href = `https://fonts.googleapis.com/css2?family=${ui.font_heading.replace(/\s/g, '+')}:wght@500;600;700&display=swap`
36
+ link.rel = 'stylesheet'
37
+ if (!document.querySelector(`link[href*="${ui.font_heading}"]`)) {
38
+ document.head.appendChild(link)
39
+ }
40
+ }
41
+
42
+ if (ui.font_body) {
43
+ root.style.setProperty('--bd-font-body', `'${ui.font_body}', sans-serif`)
44
+ // Dynamically load font
45
+ const link = document.createElement('link')
46
+ link.href = `https://fonts.googleapis.com/css2?family=${ui.font_body.replace(/\s/g, '+')}:wght@400;500;600&display=swap`
47
+ link.rel = 'stylesheet'
48
+ if (!document.querySelector(`link[href*="${ui.font_body}"]`)) {
49
+ document.head.appendChild(link)
50
+ }
51
+ }
52
+ }
53
+ }, [data])
54
+
55
+ useEffect(() => {
56
+ document.body.dataset.mounted = 'false'
57
+ const timeout = setTimeout(() => {
58
+ document.body.dataset.mounted = 'true'
59
+ }, 0)
60
+
61
+ return () => {
62
+ clearTimeout(timeout)
63
+ delete document.body.dataset.mounted
64
+ }
65
+ }, [])
66
+
67
+ useEffect(() => {
68
+ const initializeData = (payload) => {
69
+ setData(payload)
70
+ setLoading(false)
71
+
72
+ if (payload.openapi?.paths) {
73
+ const firstPath = Object.keys(payload.openapi.paths)[0]
74
+ const firstMethod = Object.keys(payload.openapi.paths[firstPath])[0]
75
+ setSelectedItem({ type: 'endpoint', path: firstPath, method: firstMethod })
76
+ } else if (payload.articles?.length > 0) {
77
+ setSelectedItem({ type: 'article', id: payload.articles[0].id })
78
+ }
79
+ }
80
+
81
+ const tryInitialize = () => {
82
+ if (window.ElderDocsData) {
83
+ initializeData(window.ElderDocsData)
84
+ return true
85
+ }
86
+ return false
87
+ }
88
+
89
+ if (!tryInitialize()) {
90
+ const handleDataLoaded = () => {
91
+ if (window.ElderDocsData) {
92
+ initializeData(window.ElderDocsData)
93
+ }
94
+ }
95
+
96
+ window.addEventListener('elderdocs:data_loaded', handleDataLoaded)
97
+
98
+ // Fallback timeout so we don't wait forever
99
+ const timeoutId = setTimeout(() => {
100
+ if (!window.ElderDocsData) {
101
+ console.error('ElderDocsData not found. Make sure data.js is loaded.')
102
+ setLoading(false)
103
+ }
104
+ }, 5000)
105
+
106
+ return () => {
107
+ window.removeEventListener('elderdocs:data_loaded', handleDataLoaded)
108
+ clearTimeout(timeoutId)
109
+ }
110
+ }
111
+ }, [])
112
+
113
+ if (loading) {
114
+ return (
115
+ <div className="flex items-center justify-center h-screen bg-white">
116
+ <div className="text-3xl font-black text-black">Loading documentation...</div>
117
+ </div>
118
+ )
119
+ }
120
+
121
+ if (!data) {
122
+ return (
123
+ <div className="flex items-center justify-center h-screen bg-white">
124
+ <div className="text-3xl font-black text-red-600">Error: Documentation data not found</div>
125
+ </div>
126
+ )
127
+ }
128
+
129
+ return (
130
+ <ApiKeyProvider>
131
+ <div className="app-shell">
132
+ <Sidebar
133
+ data={data}
134
+ activeView={activeView}
135
+ setActiveView={setActiveView}
136
+ selectedItem={selectedItem}
137
+ setSelectedItem={setSelectedItem}
138
+ />
139
+ <ContentPanel
140
+ data={data}
141
+ activeView={activeView}
142
+ selectedItem={selectedItem}
143
+ />
144
+ <ApiExplorer
145
+ data={data}
146
+ selectedItem={selectedItem}
147
+ />
148
+ </div>
149
+ </ApiKeyProvider>
150
+ )
151
+ }
152
+
153
+ export default App
154
+
@@ -0,0 +1,11 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import UiConfigurator from './components/UiConfigurator'
4
+ import './index.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')).render(
7
+ <React.StrictMode>
8
+ <UiConfigurator />
9
+ </React.StrictMode>,
10
+ )
11
+
@@ -0,0 +1,323 @@
1
+ import React, { useState } from 'react'
2
+ import { useApiKey } from '../contexts/ApiKeyContext'
3
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
4
+ import { oneLight } from 'react-syntax-highlighter/dist/cjs/styles/prism'
5
+
6
+ const ApiExplorer = ({ data, selectedItem }) => {
7
+ const { authConfig, setAuthConfig } = useApiKey()
8
+ const [params, setParams] = useState({})
9
+ const [headers, setHeaders] = useState({})
10
+ const [body, setBody] = useState('')
11
+ const [loading, setLoading] = useState(false)
12
+ const [response, setResponse] = useState(null)
13
+ const [isOpen, setIsOpen] = useState(true)
14
+
15
+ const authTypes = data?.auth_types || ['bearer', 'api_key', 'basic', 'oauth2']
16
+ const currentAuthType = authConfig?.type || 'bearer'
17
+ const isEndpoint = selectedItem && selectedItem.type === 'endpoint'
18
+ const selectedPath = isEndpoint ? selectedItem.path : ''
19
+ const selectedMethod = isEndpoint ? selectedItem.method : ''
20
+ const operation = isEndpoint ? data.openapi.paths[selectedPath]?.[selectedMethod] : null
21
+ const servers = data.api_servers?.length > 0 ? data.api_servers : (data.openapi.servers || [])
22
+ const defaultServerUrl = data.api_server || servers[0]?.url || ''
23
+ const [selectedServer, setSelectedServer] = useState(defaultServerUrl)
24
+ const [isCustomUrl, setIsCustomUrl] = useState(false)
25
+ const [customUrl, setCustomUrl] = useState('')
26
+ const fullUrl = `${isCustomUrl ? customUrl : selectedServer}${selectedPath}`
27
+
28
+ if (!isEndpoint) {
29
+ return (
30
+ <div className={`w-[360px] surface border-l-0 border-black/10 flex flex-col transition-all bg-white ${isOpen ? '' : 'hidden'}`} style={{ borderLeftWidth: '3px' }}>
31
+ <div className="p-6 border-b-0 border-black/10" style={{ borderBottomWidth: '3px' }}>
32
+ <div className="pill text-black bg-yellow-400 border-black mb-4">API Explorer</div>
33
+ <div className="text-base font-bold text-black">Select an endpoint to light up the live runner.</div>
34
+ </div>
35
+ </div>
36
+ )
37
+ }
38
+
39
+ if (!operation) return null
40
+
41
+ const handleParamChange = (name, value) => {
42
+ setParams(prev => ({ ...prev, [name]: value }))
43
+ }
44
+
45
+ const handleHeaderChange = (name, value) => {
46
+ setHeaders(prev => ({ ...prev, [name]: value }))
47
+ }
48
+
49
+ const handleAuthChange = (type, value) => {
50
+ setAuthConfig({ type, value })
51
+ }
52
+
53
+ const buildUrl = () => {
54
+ let url = fullUrl
55
+ const queryParams = []
56
+
57
+ // First, replace path parameters
58
+ Object.entries(params).forEach(([key, value]) => {
59
+ if (value) {
60
+ const param = operation.parameters?.find(p => p.name === key && p.in === 'path')
61
+ if (param) {
62
+ url = url.replace(`{${key}}`, encodeURIComponent(value))
63
+ }
64
+ }
65
+ })
66
+
67
+ // Then, add query parameters
68
+ Object.entries(params).forEach(([key, value]) => {
69
+ if (value) {
70
+ const param = operation.parameters?.find(p => p.name === key && p.in === 'query')
71
+ if (param) {
72
+ queryParams.push(`${key}=${encodeURIComponent(value)}`)
73
+ }
74
+ }
75
+ })
76
+
77
+ if (queryParams.length > 0) {
78
+ url += '?' + queryParams.join('&')
79
+ }
80
+
81
+ return url
82
+ }
83
+
84
+ const executeRequest = async () => {
85
+ setLoading(true)
86
+ setResponse(null)
87
+
88
+ try {
89
+ const url = buildUrl()
90
+ const requestHeaders = {
91
+ 'Content-Type': 'application/json',
92
+ ...headers
93
+ }
94
+
95
+ // Apply authentication based on type
96
+ if (authConfig?.value) {
97
+ switch (authConfig.type) {
98
+ case 'bearer':
99
+ requestHeaders['Authorization'] = `Bearer ${authConfig.value}`
100
+ break
101
+ case 'api_key':
102
+ requestHeaders['X-API-Key'] = authConfig.value
103
+ break
104
+ case 'basic':
105
+ requestHeaders['Authorization'] = `Basic ${btoa(authConfig.value)}`
106
+ break
107
+ case 'oauth2':
108
+ requestHeaders['Authorization'] = `Bearer ${authConfig.value}`
109
+ break
110
+ }
111
+ }
112
+
113
+ const requestOptions = {
114
+ method: selectedMethod.toUpperCase(),
115
+ headers: requestHeaders
116
+ }
117
+
118
+ if (['post', 'put', 'patch'].includes(selectedMethod.toLowerCase()) && body) {
119
+ requestOptions.body = body
120
+ }
121
+
122
+ const startTime = Date.now()
123
+ const res = await fetch(url, requestOptions)
124
+ const endTime = Date.now()
125
+
126
+ const responseText = await res.text()
127
+ let responseData
128
+ try {
129
+ responseData = JSON.parse(responseText)
130
+ } catch {
131
+ responseData = responseText
132
+ }
133
+
134
+ setResponse({
135
+ status: res.status,
136
+ statusText: res.statusText,
137
+ headers: Object.fromEntries(res.headers.entries()),
138
+ body: responseData,
139
+ time: endTime - startTime,
140
+ url: url
141
+ })
142
+ } catch (error) {
143
+ setResponse({
144
+ error: error.message
145
+ })
146
+ } finally {
147
+ setLoading(false)
148
+ }
149
+ }
150
+
151
+ const queryParams = operation.parameters?.filter(p => p.in === 'query') || []
152
+ const pathParams = operation.parameters?.filter(p => p.in === 'path') || []
153
+ const headerParams = operation.parameters?.filter(p => p.in === 'header') || []
154
+ const hasRequestBody = operation.requestBody && ['post', 'put', 'patch'].includes(selectedMethod.toLowerCase())
155
+
156
+ return (
157
+ <div className={`w-[360px] surface border-l-0 border-black/10 flex flex-col transition-all bg-white ${isOpen ? '' : 'hidden'}`} style={{ borderLeftWidth: '3px' }}>
158
+ <div className="p-6 border-b-0 border-black/10 flex items-center justify-between" style={{ borderBottomWidth: '3px' }}>
159
+ <div>
160
+ <div className="pill text-black bg-yellow-400 border-black">Live Runner</div>
161
+ <h2 className="text-xl font-black text-black mt-4 font-['Syne'] uppercase">Send real traffic</h2>
162
+ </div>
163
+ <button
164
+ onClick={() => setIsOpen(!isOpen)}
165
+ className="md:hidden btn-secondary px-3 py-2"
166
+ >
167
+ {isOpen ? 'Close' : 'Open'}
168
+ </button>
169
+ </div>
170
+
171
+ <div className="flex-1 overflow-y-auto p-6 space-y-6 text-black">
172
+ <div className="reveal" style={{ animationDelay: '140ms' }}>
173
+ <p className="text-[0.65rem] uppercase tracking-[0.3em] text-black/60 mb-2 font-bold">Base URL</p>
174
+ {!isCustomUrl ? (
175
+ <select
176
+ value={selectedServer}
177
+ onChange={(e) => {
178
+ if (e.target.value === '__custom__') {
179
+ setIsCustomUrl(true)
180
+ setCustomUrl(selectedServer)
181
+ } else {
182
+ setSelectedServer(e.target.value)
183
+ }
184
+ }}
185
+ className="input-field w-full text-sm bg-white border-black text-black font-bold"
186
+ >
187
+ {servers.map((server, idx) => (
188
+ <option key={idx} value={server.url}>
189
+ {server.description ? `${server.description} (${server.url})` : server.url}
190
+ </option>
191
+ ))}
192
+ <option value="__custom__">➕ Custom URL...</option>
193
+ </select>
194
+ ) : (
195
+ <div className="space-y-2">
196
+ <input
197
+ type="text"
198
+ value={customUrl}
199
+ onChange={(e) => setCustomUrl(e.target.value)}
200
+ placeholder="Enter custom base URL"
201
+ className="input-field w-full text-sm bg-white border-black text-black font-medium"
202
+ />
203
+ <button
204
+ onClick={() => {
205
+ setIsCustomUrl(false)
206
+ if (customUrl && servers.some(s => s.url === customUrl)) {
207
+ setSelectedServer(customUrl)
208
+ }
209
+ }}
210
+ className="btn-secondary text-xs px-3 py-1"
211
+ >
212
+ Use Preset
213
+ </button>
214
+ </div>
215
+ )}
216
+ </div>
217
+
218
+ <div className="reveal" style={{ animationDelay: '140ms' }}>
219
+ <p className="text-[0.65rem] uppercase tracking-[0.3em] text-black/60 mb-2 font-bold">Authentication</p>
220
+ <select
221
+ value={currentAuthType}
222
+ onChange={(e) => handleAuthChange(e.target.value, authConfig?.value || '')}
223
+ className="input-field w-full text-sm bg-white border-black text-black font-bold"
224
+ >
225
+ {authTypes.map(type => (
226
+ <option key={type} value={type}>
227
+ {type === 'bearer' ? 'Bearer Token' :
228
+ type === 'api_key' ? 'API Key Header' :
229
+ type === 'basic' ? 'Basic Auth' :
230
+ type === 'oauth2' ? 'OAuth 2.0' : type}
231
+ </option>
232
+ ))}
233
+ </select>
234
+ <input
235
+ type={currentAuthType === 'basic' ? 'text' : 'password'}
236
+ value={authConfig?.value || ''}
237
+ onChange={(e) => handleAuthChange(currentAuthType, e.target.value)}
238
+ placeholder={
239
+ currentAuthType === 'bearer' ? 'Enter Bearer token' :
240
+ currentAuthType === 'api_key' ? 'Enter API key' :
241
+ currentAuthType === 'basic' ? 'username:password' :
242
+ currentAuthType === 'oauth2' ? 'Enter OAuth token' : 'Enter credentials'
243
+ }
244
+ className="input-field w-full mt-3 text-sm bg-white border-black text-black font-medium"
245
+ />
246
+ </div>
247
+
248
+ {[{ label: 'Path Parameters', items: pathParams, handler: handleParamChange, values: params },
249
+ { label: 'Query Parameters', items: queryParams, handler: handleParamChange, values: params },
250
+ { label: 'Headers', items: headerParams, handler: handleHeaderChange, values: headers },
251
+ ].map((section, idx) => (
252
+ section.items.length > 0 && (
253
+ <div key={section.label} className="reveal" style={{ animationDelay: `${160 + idx * 60}ms` }}>
254
+ <p className="text-[0.65rem] uppercase tracking-[0.3em] text-black/60 mb-2 font-bold">{section.label}</p>
255
+ {section.items.map((param) => (
256
+ <div key={param.name} className="mb-3">
257
+ <label className="block text-[0.6rem] font-bold uppercase tracking-[0.25em] text-black/80 mb-1">
258
+ {param.name} {param.required && <span className="text-red-500">*</span>}
259
+ </label>
260
+ <input
261
+ type="text"
262
+ value={section.values[param.name] || ''}
263
+ onChange={(e) => section.handler(param.name, e.target.value)}
264
+ placeholder={param.schema?.default || ''}
265
+ className="input-field w-full text-sm bg-white border-black text-black font-medium"
266
+ />
267
+ </div>
268
+ ))}
269
+ </div>
270
+ )
271
+ ))}
272
+
273
+ {hasRequestBody && (
274
+ <div className="reveal" style={{ animationDelay: '260ms' }}>
275
+ <p className="text-[0.65rem] uppercase tracking-[0.3em] text-black/60 mb-2 font-bold">Request Body</p>
276
+ <textarea
277
+ value={body}
278
+ onChange={(e) => setBody(e.target.value)}
279
+ placeholder="Enter JSON body"
280
+ className="input-field w-full h-32 font-mono text-sm bg-white border-black text-black font-medium"
281
+ />
282
+ </div>
283
+ )}
284
+
285
+ <button
286
+ onClick={executeRequest}
287
+ disabled={loading}
288
+ className="btn-primary w-full reveal"
289
+ style={{ animationDelay: '300ms' }}
290
+ >
291
+ {loading ? 'Sending...' : 'Send Request'}
292
+ </button>
293
+
294
+ {response && (
295
+ <div className="reveal" style={{ animationDelay: '320ms' }}>
296
+ <p className="text-[0.65rem] uppercase tracking-[0.3em] text-black/60 mb-2 font-bold">Response</p>
297
+ {response.error ? (
298
+ <div className="surface surface--highlight border-2 border-black bg-red-50 text-red-900 text-sm p-4 font-bold rounded-none">
299
+ {response.error}
300
+ </div>
301
+ ) : (
302
+ <div className="space-y-3">
303
+ <div className="flex items-center gap-3 text-sm text-black">
304
+ <span className="chip bg-white border-black text-black">
305
+ {response.status} {response.statusText}
306
+ </span>
307
+ <span className="font-bold">{response.time} ms</span>
308
+ </div>
309
+ <div className="rounded-none border-2 border-black bg-white">
310
+ <SyntaxHighlighter language="json" style={oneLight} customStyle={{ margin: 0, padding: '16px', fontSize: '14px', backgroundColor: '#fff' }}>
311
+ {JSON.stringify(response.body, null, 2)}
312
+ </SyntaxHighlighter>
313
+ </div>
314
+ </div>
315
+ )}
316
+ </div>
317
+ )}
318
+ </div>
319
+ </div>
320
+ )
321
+ }
322
+
323
+ export default ApiExplorer