Phase 3 part 1: Add interactive API explorer - Create templates/api_explorer.templ with mock and real modes - Add EndpointInfo and APIExplorerData types to templates/types.go - Fix markdown rendering with UnsafeHTML.ToComponent() - Add templ import for Component support - API explorer supports: - Mock data mode for non-authenticated users - Real execution mode for logged-in users - Request/response display - cURL generation - Copy to clipboard
283 lines
8.1 KiB
Templ
283 lines
8.1 KiB
Templ
package templates
|
|
|
|
templ APIExplorer(explorer APIExplorerData) {
|
|
if !explorer.IsLoggedIn {
|
|
// Show mode toggle and mock data
|
|
<div class="api-explorer">
|
|
<div class="api-mode-toggle">
|
|
<button class="mode-btn active">Mock Data</button>
|
|
<button class="mode-btn" disabled>Login to Try Real</button>
|
|
</div>
|
|
<div class="api-request-editor">
|
|
<div class="method-selector">
|
|
<select disabled>
|
|
<option value="GET" selected?={ explorer.Endpoint.Method == "GET" }>GET</option>
|
|
<option value="POST" selected?={ explorer.Endpoint.Method == "POST" }>POST</option>
|
|
<option value="PUT" selected?={ explorer.Endpoint.Method == "PUT" }>PUT</option>
|
|
<option value="DELETE" selected?={ explorer.Endpoint.Method == "DELETE" }>DELETE</option>
|
|
</select>
|
|
</div>
|
|
<h4>Request Body (Example)</h4>
|
|
<pre><code>{ explorer.Endpoint.RequestBody }</code></pre>
|
|
</div>
|
|
<div class="api-response">
|
|
<h4>Response (Mock)</h4>
|
|
<pre><code>{ explorer.Endpoint.Response }</code></pre>
|
|
</div>
|
|
<button onclick="copyToClipboard(JSON.stringify({ explorer.Endpoint.RequestBody }, null, 2))">Copy Request</button>
|
|
<button onclick="copyToClipboard(JSON.stringify({ explorer.Endpoint.Response }, null, 2))">Copy Response</button>
|
|
</div>
|
|
} else {
|
|
// Show interactive explorer with real execution
|
|
<div class="api-explorer">
|
|
<div class="api-mode-toggle">
|
|
<button class="mode-btn" id="mock-btn">Mock Data</button>
|
|
<button class="mode-btn active" id="real-btn">Try It Out</button>
|
|
</div>
|
|
<div class="api-request-editor">
|
|
<div class="method-selector">
|
|
<select id="http-method">
|
|
<option value="GET" selected?={ explorer.Endpoint.Method == "GET" }>GET</option>
|
|
<option value="POST" selected?={ explorer.Endpoint.Method == "POST" }>POST</option>
|
|
<option value="PUT" selected?={ explorer.Endpoint.Method == "PUT" }>PUT</option>
|
|
<option value="DELETE" selected?={ explorer.Endpoint.Method == "DELETE" }>DELETE</option>
|
|
</select>
|
|
</div>
|
|
<h4>Request Body</h4>
|
|
<textarea id="request-body" placeholder="Edit request body...">{ explorer.Endpoint.RequestBody }</textarea>
|
|
</div>
|
|
<button id="try-it-out">Try It Out</button>
|
|
<div class="api-response" style="display:none">
|
|
<div class="response-header">
|
|
<span id="response-status"></span>
|
|
<span id="response-time"></span>
|
|
</div>
|
|
<pre><code id="response-body"></code></pre>
|
|
</div>
|
|
<button onclick="copyRequest()">Copy Request</button>
|
|
<button onclick="copyResponse()">Copy Response</button>
|
|
<button onclick="generateCURL()">Generate cURL</button>
|
|
</div>
|
|
}
|
|
|
|
<script>
|
|
const endpointPath = '{ explorer.Endpoint.Path }';
|
|
const exampleRequest = { explorer.Endpoint.RequestBody };
|
|
const exampleResponse = { explorer.Endpoint.Response };
|
|
|
|
// Mode toggle logic
|
|
document.getElementById('mock-btn')?.addEventListener('click', () => showMode('mock'));
|
|
document.getElementById('real-btn')?.addEventListener('click', () => showMode('real'));
|
|
|
|
function showMode(mode) {
|
|
if (mode === 'mock') {
|
|
document.querySelector('.api-response').style.display = 'block';
|
|
document.getElementById('request-body').readOnly = true;
|
|
document.getElementById('try-it-out').style.display = 'none';
|
|
document.getElementById('response-body').textContent = JSON.stringify(exampleResponse, null, 2);
|
|
document.getElementById('response-status').textContent = '200 OK';
|
|
document.getElementById('response-time').textContent = 'Mock';
|
|
} else {
|
|
document.querySelector('.api-response').style.display = 'none';
|
|
document.getElementById('request-body').readOnly = false;
|
|
document.getElementById('try-it-out').style.display = 'block';
|
|
}
|
|
}
|
|
|
|
// Try it out logic
|
|
document.getElementById('try-it-out')?.addEventListener('click', async () => {
|
|
const method = document.getElementById('http-method').value;
|
|
const body = document.getElementById('request-body').value;
|
|
|
|
const startTime = Date.now();
|
|
try {
|
|
const response = await fetch(endpointPath, {
|
|
method: method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
|
},
|
|
body: ['GET', 'DELETE'].includes(method) ? undefined : body
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
const data = await response.json();
|
|
|
|
document.getElementById('response-status').textContent = `${response.status} (${response.statusText})`;
|
|
document.getElementById('response-time').textContent = `${duration}ms`;
|
|
document.getElementById('response-body').textContent = JSON.stringify(data, null, 2);
|
|
document.querySelector('.api-response').style.display = 'block';
|
|
} catch (error) {
|
|
document.getElementById('response-status').textContent = 'Error';
|
|
document.getElementById('response-body').textContent = error.message;
|
|
document.querySelector('.api-response').style.display = 'block';
|
|
}
|
|
});
|
|
|
|
function copyToClipboard(text) {
|
|
navigator.clipboard.writeText(text);
|
|
}
|
|
|
|
function copyRequest() {
|
|
const body = document.getElementById('request-body').value;
|
|
navigator.clipboard.writeText(body);
|
|
}
|
|
|
|
function copyResponse() {
|
|
const body = document.getElementById('response-body').textContent;
|
|
navigator.clipboard.writeText(body);
|
|
}
|
|
|
|
function generateCURL() {
|
|
const method = document.getElementById('http-method').value;
|
|
const body = document.getElementById('request-body').value;
|
|
const token = localStorage.getItem('token');
|
|
|
|
let curl = `curl -X ${method} \\n -H "Content-Type: application/json" \\n -H "Authorization: Bearer ${token}"`;
|
|
|
|
if (!['GET', 'DELETE'].includes(method) && body.trim()) {
|
|
curl += ` \\n -d '${body}'`;
|
|
}
|
|
|
|
curl += ` \\n ${endpointPath}`;
|
|
|
|
navigator.clipboard.writeText(curl);
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.api-explorer {
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 1.5rem;
|
|
margin-top: 2rem;
|
|
background: var(--bg-secondary);
|
|
}
|
|
|
|
.api-mode-toggle {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.mode-btn {
|
|
padding: 0.5rem 1rem;
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.mode-btn.active {
|
|
background: var(--accent);
|
|
color: white;
|
|
}
|
|
|
|
.mode-btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.api-request-editor {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.method-selector select {
|
|
padding: 0.5rem;
|
|
border-radius: 4px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
border: 1px solid var(--border);
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.api-request-editor h4 {
|
|
margin-bottom: 0.5rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
textarea#request-body {
|
|
width: 100%;
|
|
min-height: 150px;
|
|
padding: 0.75rem;
|
|
font-family: 'Monaco', 'Consolas', monospace;
|
|
font-size: 0.875rem;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.api-response {
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.response-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 0.5rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
#response-status {
|
|
font-weight: 600;
|
|
}
|
|
|
|
#response-time {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.api-response pre {
|
|
background: var(--bg-primary);
|
|
padding: 1rem;
|
|
border-radius: 4px;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.api-response code {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.api-explorer button {
|
|
padding: 0.5rem 1rem;
|
|
margin-right: 0.5rem;
|
|
margin-top: 1rem;
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
cursor: pointer;
|
|
}
|
|
|
|
#try-it-out {
|
|
background: var(--accent);
|
|
color: white;
|
|
padding: 0.75rem 1.5rem;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
#try-it-out:hover {
|
|
opacity: 0.9;
|
|
}
|
|
</style>
|
|
}
|
|
|
|
// APIExplorerData holds data for the API explorer component
|
|
type APIExplorerData struct {
|
|
Endpoint EndpointInfo
|
|
IsLoggedIn bool
|
|
}
|
|
|
|
// EndpointInfo holds information about an API endpoint
|
|
type EndpointInfo struct {
|
|
Method string
|
|
Path string
|
|
RequestBody string
|
|
Response string
|
|
Description string
|
|
}
|