1381 lines
63 KiB
Vue

<script setup>
import { useRoute } from 'vue-router'
definePageMeta({
title: "API Documentation",
layout: "empty",
});
// Import route to check for preview mode
const route = useRoute()
// API Documentation state
const apiSpec = ref(null)
const isLoading = ref(true)
const isError = ref(false)
const errorMessage = ref('')
const searchQuery = ref('')
const selectedEndpoint = ref(null)
const expandedTags = ref(new Set())
const activeTab = ref('overview')
const selectedLanguage = ref('javascript')
const copyNotification = ref(false)
// Try It Out functionality
const isTryingEndpoint = ref(false)
const endpointResponse = ref(null)
const endpointResponseStatus = ref(null)
const endpointResponseTime = ref(null)
const isEndpointLoading = ref(false)
const authToken = ref('')
const paramValues = ref({})
const requestBodyValue = ref('')
const showAuthModal = ref(false)
const selectedBaseUrl = ref('')
const customBaseUrl = ref('')
// Syntax highlighting for code examples
const getCodeClass = (language = '') => {
switch (language.toLowerCase()) {
case 'javascript': return 'language-javascript'
case 'python': return 'language-python'
case 'php': return 'language-php'
case 'ruby': return 'language-ruby'
case 'curl': return 'language-bash'
default: return 'language-javascript'
}
}
// Basic syntax highlighter
const highlightCode = (code, language) => {
if (!code) return ''
// Basic implementation to add highlighting
// First escape HTML entities to prevent XSS
const escapeHTML = (str) => {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
// Escape HTML first
let escapedCode = escapeHTML(code)
// Common patterns
const patterns = {
string: /(["'`])(?:(?=(\\?))\2.)*?\1/g,
comment: /\/\/.+|\/\*[\s\S]*?\*\//g,
keyword: /\b(const|let|var|function|return|if|else|for|while|try|catch|new|class|import|export|async|await|from)\b/g,
number: /\b\d+\.?\d*\b/g,
boolean: /\b(true|false|null|undefined)\b/g,
}
// Language-specific patterns
switch (language.toLowerCase()) {
case 'javascript':
// JavaScript syntax
return escapedCode
.replace(patterns.comment, '<span class="comment">$&</span>')
.replace(patterns.string, '<span class="string">$&</span>')
.replace(patterns.keyword, '<span class="keyword">$&</span>')
.replace(patterns.number, '<span class="number">$&</span>')
.replace(patterns.boolean, '<span class="boolean">$&</span>')
.replace(/\b(fetch|then|catch|console|log|error)\b/g, '<span class="function">$&</span>')
.replace(/(\(|\)|\{|\}|\[|\]|=>)/g, '<span class="bracket">$&</span>');
case 'python':
// Python syntax
return escapedCode
.replace(/#.+/g, '<span class="comment">$&</span>')
.replace(patterns.string, '<span class="string">$&</span>')
.replace(/\b(import|from|def|class|if|else|elif|for|while|try|except|return|and|or|not|in|is)\b/g, '<span class="keyword">$&</span>')
.replace(patterns.number, '<span class="number">$&</span>')
.replace(/\b(True|False|None)\b/g, '<span class="boolean">$&</span>')
.replace(/\b(print|requests|json|response)\b/g, '<span class="function">$&</span>')
.replace(/(\(|\)|\{|\}|\[|\]|:)/g, '<span class="bracket">$&</span>');
case 'php':
// PHP syntax
return escapedCode
.replace(/(<\?php|\?>)/g, '<span class="php-tag">$&</span>')
.replace(patterns.comment, '<span class="comment">$&</span>')
.replace(patterns.string, '<span class="string">$&</span>')
.replace(/\b(function|if|else|foreach|while|echo|return|try|catch|new|class|use|namespace)\b/g, '<span class="keyword">$&</span>')
.replace(patterns.number, '<span class="number">$&</span>')
.replace(/\b(true|false|null)\b/g, '<span class="boolean">$&</span>')
.replace(/\b(array|curl_init|curl_setopt|curl_exec|curl_close|json_encode)\b/g, '<span class="function">$&</span>')
.replace(/(\(|\)|\{|\}|\[|\]|;)/g, '<span class="bracket">$&</span>');
case 'ruby':
// Ruby syntax
return escapedCode
.replace(/#.+/g, '<span class="comment">$&</span>')
.replace(patterns.string, '<span class="string">$&</span>')
.replace(/\b(def|class|if|else|elsif|end|begin|rescue|require|module|attr_accessor|attr_reader)\b/g, '<span class="keyword">$&</span>')
.replace(patterns.number, '<span class="number">$&</span>')
.replace(/\b(true|false|nil)\b/g, '<span class="boolean">$&</span>')
.replace(/\b(puts|require|URI|Net::HTTP|JSON|to_json)\b/g, '<span class="function">$&</span>')
.replace(/(\(|\)|\{|\}|\[|\]|do\b|\|)/g, '<span class="bracket">$&</span>');
case 'curl':
case 'bash':
// cURL/bash syntax
return escapedCode
.replace(/#.+/g, '<span class="comment">$&</span>')
.replace(/'[^']*'|"[^"]*"/g, '<span class="string">$&</span>')
.replace(/\b(curl|echo|if|then|else|fi|for|while|do|done)\b/g, '<span class="keyword">$&</span>')
.replace(/(-X|-H|--header|-d|--data|-F|--form)\b/g, '<span class="param">$&</span>')
.replace(/\\$/gm, '<span class="escape">$&</span>');
default:
return escapedCode;
}
}
// Create HTML syntax highlighted code
const getHighlightedCode = computed(() => {
if (!selectedEndpoint.value) return ''
const code = generateCodeExample(selectedEndpoint.value)
return highlightCode(code, selectedLanguage.value)
})
// Base URLs from server definitions
const availableBaseUrls = computed(() => {
if (!apiSpec.value?.servers) return [{ url: window.location.origin, name: 'Current Origin' }]
const urls = apiSpec.value.servers.map((server, index) => {
// Process variables in the URL if any
let url = server.url
let name = server.description || `Server ${index + 1}`
// Replace variables with defaults
if (server.variables) {
Object.entries(server.variables).forEach(([varName, varConfig]) => {
const defaultValue = varConfig.default || ''
url = url.replace(`{${varName}}`, defaultValue)
})
}
return { url, name, originalDefinition: server }
})
// Add current origin as an option
urls.push({ url: window.location.origin, name: 'Current Origin' })
// Add custom URL option
urls.push({ url: 'custom', name: 'Custom URL...' })
return urls
})
// Current effective base URL
const effectiveBaseUrl = computed(() => {
if (selectedBaseUrl.value === 'custom') {
return customBaseUrl.value || window.location.origin
}
return selectedBaseUrl.value || (availableBaseUrls.value[0]?.url || window.location.origin)
})
// Check if this is preview mode
const isPreviewMode = computed(() => route.query.preview === 'true')
// Theme and configuration
const {
scalarConfig,
loadScalarConfig,
getScalarConfiguration
} = useScalarConfig()
// Available programming languages for code examples
const availableLanguages = [
{ value: 'javascript', label: 'JavaScript', icon: 'logos:javascript' },
{ value: 'python', label: 'Python', icon: 'logos:python' },
{ value: 'php', label: 'PHP', icon: 'logos:php' },
{ value: 'curl', label: 'cURL', icon: 'simple-icons:curl' },
{ value: 'ruby', label: 'Ruby', icon: 'logos:ruby' }
]
// Computed properties
const filteredEndpoints = computed(() => {
if (!apiSpec.value?.paths) return []
const endpoints = []
Object.entries(apiSpec.value.paths).forEach(([path, methods]) => {
Object.entries(methods).forEach(([method, details]) => {
if (typeof details === 'object' && details.operationId) {
endpoints.push({
path,
method: method.toUpperCase(),
summary: details.summary || '',
description: details.description || '',
tags: details.tags || [],
operationId: details.operationId,
parameters: details.parameters || [],
requestBody: details.requestBody,
responses: details.responses || {},
security: details.security || [],
details
})
}
})
})
if (!searchQuery.value) return endpoints
const query = searchQuery.value.toLowerCase()
return endpoints.filter(endpoint =>
endpoint.path.toLowerCase().includes(query) ||
endpoint.summary.toLowerCase().includes(query) ||
endpoint.description.toLowerCase().includes(query) ||
endpoint.tags.some(tag => tag.toLowerCase().includes(query))
)
})
const groupedEndpoints = computed(() => {
const groups = {}
filteredEndpoints.value.forEach(endpoint => {
const tag = endpoint.tags[0] || 'Other'
if (!groups[tag]) {
groups[tag] = []
}
groups[tag].push(endpoint)
})
return groups
})
const methodColors = {
'GET': 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-900/20 dark:text-emerald-300 dark:border-emerald-800',
'POST': 'bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-300 dark:border-blue-800',
'PUT': 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-900/20 dark:text-amber-300 dark:border-amber-800',
'DELETE': 'bg-red-50 text-red-700 border-red-200 dark:bg-red-900/20 dark:text-red-300 dark:border-red-800',
'PATCH': 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-900/20 dark:text-purple-300 dark:border-purple-800'
}
// Reset try-it-out form when endpoint changes
watch(selectedEndpoint, () => {
paramValues.value = {}
requestBodyValue.value = ''
endpointResponse.value = null
endpointResponseStatus.value = null
endpointResponseTime.value = null
isTryingEndpoint.value = false
// Initialize parameter values
if (selectedEndpoint.value?.parameters) {
selectedEndpoint.value.parameters.forEach(param => {
if (param.example !== undefined) {
paramValues.value[param.name] = param.example
} else if (param.schema?.default !== undefined) {
paramValues.value[param.name] = param.schema.default
} else {
paramValues.value[param.name] = ''
}
})
}
// Initialize request body if available
if (selectedEndpoint.value?.requestBody?.content) {
const contentType = Object.keys(selectedEndpoint.value.requestBody.content)[0]
const example = selectedEndpoint.value.requestBody.content[contentType]?.example
if (example) {
requestBodyValue.value = JSON.stringify(example, null, 2)
} else {
requestBodyValue.value = '{}'
}
}
})
// Generate code examples for different languages
const generateCodeExample = (endpoint, language = null) => {
const baseUrl = apiSpec.value?.servers?.[0]?.url || 'https://api.example.com'
const fullUrl = `${baseUrl}${endpoint.path}`
const lang = language || endpoint.__lang || selectedLanguage.value
const examples = {
javascript: `// Using fetch API
fetch('${fullUrl}', {
method: '${endpoint.method}',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
}${endpoint.requestBody ? ',\n body: JSON.stringify({\n // Your request data\n })' : ''}
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));`,
python: `import requests
url = "${fullUrl}"
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_TOKEN"
}
response = requests.${endpoint.method.toLowerCase()}(url, headers=headers${endpoint.requestBody ? ', json={}' : ''})
print(response.json())`,
php: `<?php
$url = "${fullUrl}";
$headers = array(
"Content-Type: application/json",
"Authorization: Bearer YOUR_TOKEN"
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
${endpoint.method !== 'GET' ? `curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "${endpoint.method}");` : ''}
${endpoint.requestBody ? 'curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(array()));' : ''}
$response = curl_exec($ch);
curl_close($ch);
echo $response;
?>`,
curl: `curl -X ${endpoint.method} "${fullUrl}" \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer YOUR_TOKEN"${endpoint.requestBody ? ' \\\n -d \'{\n "key": "value"\n }\'' : ''}`,
ruby: `require 'net/http'
require 'json'
uri = URI('${fullUrl}')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::${endpoint.method.charAt(0) + endpoint.method.slice(1).toLowerCase()}(uri)
request['Content-Type'] = 'application/json'
request['Authorization'] = 'Bearer YOUR_TOKEN'
${endpoint.requestBody ? 'request.body = {}.to_json' : ''}
response = http.request(request)
puts response.body`
}
return examples[lang] || examples.javascript
}
// Execute API request for Try It Out
const executeRequest = async () => {
if (!selectedEndpoint.value) return
isEndpointLoading.value = true
endpointResponse.value = null
endpointResponseStatus.value = null
try {
// Use the effective base URL
const baseUrl = effectiveBaseUrl.value
let url = `${baseUrl}${selectedEndpoint.value.path}`
// Replace path parameters
const pathParams = selectedEndpoint.value.parameters?.filter(p => p.in === 'path') || []
pathParams.forEach(param => {
if (paramValues.value[param.name]) {
url = url.replace(`{${param.name}}`, encodeURIComponent(paramValues.value[param.name]))
}
})
// Add query parameters
const queryParams = selectedEndpoint.value.parameters?.filter(p => p.in === 'query') || []
if (queryParams.length > 0) {
const queryString = queryParams
.filter(param => paramValues.value[param.name])
.map(param => `${param.name}=${encodeURIComponent(paramValues.value[param.name])}`)
.join('&')
if (queryString) {
url += url.includes('?') ? `&${queryString}` : `?${queryString}`
}
}
// Prepare headers
const headers = {
'Accept': 'application/json, text/plain, */*',
}
// Add authorization if provided
if (authToken.value) {
headers['Authorization'] = `Bearer ${authToken.value}`
}
// Add header parameters
const headerParams = selectedEndpoint.value.parameters?.filter(p => p.in === 'header') || []
headerParams.forEach(param => {
if (paramValues.value[param.name]) {
headers[param.name] = paramValues.value[param.name]
}
})
// Prepare request options
const options = {
method: selectedEndpoint.value.method,
headers,
credentials: 'include', // Include cookies for same-origin requests
}
// Add request body for non-GET requests if needed
if (selectedEndpoint.value.method !== 'GET' && selectedEndpoint.value.requestBody && requestBodyValue.value) {
try {
// Add content-type header if not present
if (!headers['Content-Type']) {
// Determine content type from the endpoint spec
const contentTypes = Object.keys(selectedEndpoint.value.requestBody.content || {})
if (contentTypes.includes('application/json')) {
headers['Content-Type'] = 'application/json'
// Parse and stringify to validate JSON
const parsedBody = JSON.parse(requestBodyValue.value)
options.body = JSON.stringify(parsedBody)
} else if (contentTypes.length > 0) {
// Use the first available content type
headers['Content-Type'] = contentTypes[0]
options.body = requestBodyValue.value
} else {
// Default to JSON if no content types specified
headers['Content-Type'] = 'application/json'
const parsedBody = JSON.parse(requestBodyValue.value)
options.body = JSON.stringify(parsedBody)
}
} else {
// Content-type already set from header params
options.body = requestBodyValue.value
}
} catch (e) {
throw new Error(`Invalid JSON format in request body: ${e.message}`)
}
}
console.log(`Making ${selectedEndpoint.value.method} request to ${url}`, options)
const startTime = Date.now()
// Make the request
try {
const response = await fetch(url, options)
const responseTime = Date.now() - startTime
endpointResponseTime.value = responseTime
// Store response status
endpointResponseStatus.value = response.status
// Parse response body
try {
const contentType = response.headers.get('content-type')
if (contentType && contentType.includes('application/json')) {
endpointResponse.value = await response.json()
} else {
endpointResponse.value = await response.text()
}
} catch (e) {
endpointResponse.value = await response.text() || 'No response body'
}
} catch (e) {
// Handle CORS and network errors
console.error('Network or CORS error:', e)
endpointResponseStatus.value = 0
endpointResponse.value = {
error: "Network or CORS error",
message: e.message,
note: "This may be due to CORS restrictions. Try using the API directly from your application instead."
}
}
// Show response tab automatically
activeTab.value = 'responses'
isTryingEndpoint.value = false
} catch (error) {
console.error('API request error:', error)
endpointResponseStatus.value = 0
endpointResponse.value = { error: error.message }
} finally {
isEndpointLoading.value = false
}
}
// Copy to clipboard functionality
const copyToClipboard = async (text) => {
try {
await navigator.clipboard.writeText(text)
copyNotification.value = true
setTimeout(() => {
copyNotification.value = false
}, 2000)
} catch (err) {
console.error('Failed to copy text: ', err)
}
}
// Load API specification
const loadApiSpec = async () => {
isLoading.value = true
isError.value = false
try {
const response = await fetch('/api/openapi-file')
if (!response.ok) {
throw new Error(`Failed to fetch API specification: ${response.status}`)
}
apiSpec.value = await response.json()
console.log('API specification loaded:', apiSpec.value)
} catch (error) {
console.error('Failed to load API specification:', error)
isError.value = true
errorMessage.value = error.message
} finally {
isLoading.value = false
}
}
// Toggle tag expansion
const toggleTag = (tag) => {
if (expandedTags.value.has(tag)) {
expandedTags.value.delete(tag)
} else {
expandedTags.value.add(tag)
}
}
// Select endpoint
const selectEndpoint = (endpoint) => {
selectedEndpoint.value = endpoint
activeTab.value = 'overview'
}
// Initialize
onMounted(() => {
loadScalarConfig()
// Check if this is preview mode
if (isPreviewMode.value) {
try {
const previewConfig = localStorage.getItem('api-docs-preview-config')
if (previewConfig) {
const parsed = JSON.parse(previewConfig)
// Update the current config temporarily for preview
Object.assign(scalarConfig.value, parsed)
}
} catch (error) {
console.error('Failed to load preview config:', error)
}
}
loadApiSpec()
// Expand all tags by default
setTimeout(() => {
Object.keys(groupedEndpoints.value).forEach(tag => {
expandedTags.value.add(tag)
})
}, 100)
})
// After API spec is loaded, set default base URL
watch(apiSpec, (newSpec) => {
if (newSpec?.servers?.length) {
let baseUrl = newSpec.servers[0].url
// Replace variables with defaults
if (newSpec.servers[0].variables) {
Object.entries(newSpec.servers[0].variables).forEach(([varName, varConfig]) => {
const defaultValue = varConfig.default || ''
baseUrl = baseUrl.replace(`{${varName}}`, defaultValue)
})
}
selectedBaseUrl.value = baseUrl
} else {
selectedBaseUrl.value = window.location.origin
}
}, { immediate: true })
</script>
<template>
<div class="min-h-screen bg-slate-50 dark:bg-slate-900 font-inter">
<!-- Copy Notification -->
<Transition
enter-active-class="transition ease-out duration-300"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-200"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<div
v-if="copyNotification"
class="fixed top-4 right-4 z-50 bg-emerald-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center space-x-2"
>
<Icon name="ic:outline-check" size="16" />
<span class="text-sm font-medium">Copied to clipboard!</span>
</div>
</Transition>
<!-- Authentication Modal -->
<Transition
enter-active-class="transition ease-out duration-300"
enter-from-class="transform opacity-0"
enter-to-class="transform opacity-100"
leave-active-class="transition ease-in duration-200"
leave-from-class="transform opacity-100"
leave-to-class="transform opacity-0"
>
<div v-if="showAuthModal" class="fixed inset-0 z-50">
<!-- Backdrop with click handler -->
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm" @click="showAuthModal = false"></div>
<!-- Modal content -->
<div class="fixed inset-0 flex items-center justify-center pointer-events-none">
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg max-w-md w-full p-6 m-4 pointer-events-auto">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">Authentication</h3>
<button @click="showAuthModal = false" class="text-slate-500 hover:text-slate-700 dark:hover:text-slate-300">
<Icon name="ic:outline-close" size="20" />
</button>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Bearer Token</label>
<input
v-model="authToken"
type="text"
class="w-full border border-slate-300 dark:border-slate-600 rounded-lg px-3 py-2 bg-white dark:bg-slate-700 text-slate-900 dark:text-white"
placeholder="Enter your auth token here"
/>
</div>
<div class="text-xs text-slate-500 dark:text-slate-400 mb-4">
<p>This token will be added to your requests as:</p>
<code class="block bg-slate-100 dark:bg-slate-700 p-2 rounded mt-1 font-mono">Authorization: Bearer {token}</code>
</div>
<button
@click="showAuthModal = false"
class="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors"
>
Save and Close
</button>
</div>
</div>
</div>
</Transition>
<!-- Preview Mode Banner -->
<div
v-if="isPreviewMode"
class="bg-gradient-to-r from-amber-400 to-orange-500 text-white px-4 py-3 text-center"
>
<div class="flex items-center justify-center space-x-2">
<Icon name="ic:outline-visibility" size="18" />
<span class="text-sm font-medium">Preview Mode - This is how your API documentation will look with the current settings</span>
</div>
</div>
<!-- Sticky Header -->
<header class="sticky top-0 z-40 bg-white/95 dark:bg-slate-800/95 backdrop-blur-sm border-b border-slate-200 dark:border-slate-700">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<!-- Logo and Title -->
<div class="flex items-center space-x-4">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center">
<Icon name="ic:outline-api" size="18" class="text-white" />
</div>
<div>
<h1 class="text-lg font-semibold text-slate-900 dark:text-white">
{{ apiSpec?.info?.title || 'API Documentation' }}
</h1>
<div class="flex items-center space-x-2">
<span v-if="apiSpec?.info?.version" class="px-2 py-0.5 text-xs font-medium bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300 rounded">
v{{ apiSpec.info.version }}
</span>
</div>
</div>
</div>
</div>
<!-- Search and Controls -->
<div class="flex items-center space-x-4">
<!-- Search -->
<div class="relative">
<Icon name="ic:outline-search" size="16" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400" />
<input
v-model="searchQuery"
type="text"
placeholder="Search endpoints..."
class="pl-10 pr-4 py-2 w-80 border border-slate-200 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-500 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
/>
</div>
<!-- Language Selector -->
<select
v-model="selectedLanguage"
class="px-3 py-2 border border-slate-200 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option v-for="lang in availableLanguages" :key="lang.value" :value="lang.value">
{{ lang.label }}
</option>
</select>
</div>
</div>
</div>
</header>
<!-- Loading State -->
<div v-if="isLoading" class="flex items-center justify-center h-96">
<div class="text-center">
<div class="w-12 h-12 mx-auto mb-4 border-4 border-blue-200 border-t-blue-600 rounded-full animate-spin"></div>
<p class="text-slate-600 dark:text-slate-400 font-medium">Loading API documentation...</p>
</div>
</div>
<!-- Error State -->
<div v-else-if="isError" class="max-w-2xl mx-auto mt-16 p-6">
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl p-8 text-center">
<div class="w-16 h-16 mx-auto mb-4 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center">
<Icon name="ic:outline-error" size="24" class="text-red-600 dark:text-red-400" />
</div>
<h3 class="text-lg font-semibold text-red-800 dark:text-red-200 mb-2">Failed to load API documentation</h3>
<p class="text-red-600 dark:text-red-400 mb-6">{{ errorMessage }}</p>
<button
@click="loadApiSpec"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors"
>
Try Again
</button>
</div>
</div>
<!-- Main Content -->
<div v-else class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
<!-- Enhanced Sidebar -->
<div class="lg:col-span-1">
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 sticky top-24">
<!-- API Info Section -->
<div class="p-6 border-b border-slate-200 dark:border-slate-700">
<h2 class="text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide mb-3">Overview</h2>
<div v-if="apiSpec?.info?.description" class="mb-4 p-4 bg-slate-50 dark:bg-slate-700/50 rounded-lg">
<p class="text-sm text-slate-600 dark:text-slate-400 leading-relaxed">{{ apiSpec.info.description }}</p>
</div>
<!-- Server Info -->
<div v-if="apiSpec?.servers?.length" class="space-y-2">
<h4 class="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide">Base URL</h4>
<div v-for="server in apiSpec.servers" :key="server.url" class="font-mono text-sm text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20 px-3 py-2 rounded-lg">
{{ server.url }}
</div>
</div>
</div>
<!-- Navigation -->
<div class="p-6">
<h2 class="text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide mb-4">Endpoints</h2>
<!-- Endpoint Groups -->
<div class="space-y-2">
<div v-for="(endpoints, tag) in groupedEndpoints" :key="tag" class="border border-slate-200 dark:border-slate-600 rounded-lg overflow-hidden">
<button
@click="toggleTag(tag)"
class="w-full px-4 py-3 bg-slate-50 dark:bg-slate-700/50 hover:bg-slate-100 dark:hover:bg-slate-700 flex items-center justify-between text-left transition-colors group"
>
<span class="font-medium text-slate-900 dark:text-white capitalize text-sm">{{ tag }}</span>
<div class="flex items-center space-x-2">
<span class="text-xs text-slate-500 bg-slate-200 dark:bg-slate-600 px-2 py-1 rounded">{{ endpoints.length }}</span>
<Icon
:name="expandedTags.has(tag) ? 'ic:outline-expand-less' : 'ic:outline-expand-more'"
size="16"
class="text-slate-500 group-hover:text-slate-700 dark:group-hover:text-slate-300 transition-colors"
/>
</div>
</button>
<div v-show="expandedTags.has(tag)" class="divide-y divide-slate-200 dark:divide-slate-600">
<button
v-for="endpoint in endpoints"
:key="`${endpoint.method}-${endpoint.path}`"
@click="selectEndpoint(endpoint)"
class="w-full px-4 py-3 text-left hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors group"
:class="{ 'bg-blue-50 dark:bg-blue-900/20 border-r-2 border-blue-500': selectedEndpoint?.operationId === endpoint.operationId }"
>
<div class="flex items-center space-x-2 mb-2">
<span class="px-2 py-1 text-xs font-medium border rounded" :class="methodColors[endpoint.method]">
{{ endpoint.method }}
</span>
</div>
<div class="space-y-1">
<code class="text-xs text-slate-600 dark:text-slate-400 block truncate">{{ endpoint.path }}</code>
<p class="text-sm text-slate-700 dark:text-slate-300 truncate">{{ endpoint.summary }}</p>
</div>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Enhanced Main Content -->
<div class="lg:col-span-3">
<div v-if="!selectedEndpoint" class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 p-12 text-center">
<div class="w-16 h-16 mx-auto mb-4 bg-slate-100 dark:bg-slate-700 rounded-full flex items-center justify-center">
<Icon name="ic:outline-touch-app" size="24" class="text-slate-400" />
</div>
<h3 class="text-xl font-semibold text-slate-900 dark:text-white mb-2">Select an endpoint</h3>
<p class="text-slate-600 dark:text-slate-400">Choose an endpoint from the sidebar to view its documentation and try it out</p>
</div>
<!-- Enhanced Endpoint Details -->
<div v-else class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden">
<!-- Endpoint Header -->
<div class="p-6 border-b border-slate-200 dark:border-slate-700 bg-gradient-to-r from-slate-50 to-white dark:from-slate-800 dark:to-slate-700">
<div class="flex items-start justify-between mb-4">
<div class="flex items-center space-x-3">
<span class="px-3 py-1 text-sm font-medium border rounded-lg" :class="methodColors[selectedEndpoint.method]">
{{ selectedEndpoint.method }}
</span>
<code class="text-xl font-mono text-slate-900 dark:text-white bg-slate-100 dark:bg-slate-600 px-3 py-1 rounded-lg">{{ selectedEndpoint.path }}</code>
</div>
<button
@click="copyToClipboard(generateCodeExample(selectedEndpoint))"
class="px-3 py-1 text-sm font-medium bg-blue-50 hover:bg-blue-100 text-blue-700 border border-blue-200 rounded-lg transition-colors flex items-center space-x-2"
>
<Icon name="ic:outline-content-copy" size="14" />
<span>Copy Code</span>
</button>
</div>
<h2 class="text-2xl font-bold text-slate-900 dark:text-white mb-2">{{ selectedEndpoint.summary }}</h2>
<p v-if="selectedEndpoint.description" class="text-slate-600 dark:text-slate-400 leading-relaxed">{{ selectedEndpoint.description }}</p>
</div>
<!-- Enhanced Tab Navigation -->
<div class="border-b border-slate-200 dark:border-slate-700">
<nav class="flex space-x-8 px-6" aria-label="Tabs">
<button
v-for="tab in [
{ id: 'overview', name: 'Overview', icon: 'ic:outline-info' },
{ id: 'parameters', name: 'Parameters', icon: 'ic:outline-settings' },
{ id: 'body', name: 'Request Body', icon: 'ic:outline-article' },
{ id: 'responses', name: 'Responses', icon: 'ic:outline-check-circle' },
{ id: 'examples', name: 'Code Examples', icon: 'ic:outline-code' }
]"
:key="tab.id"
@click="activeTab = tab.id"
class="flex items-center space-x-2 py-4 px-1 border-b-2 font-medium text-sm transition-colors"
:class="activeTab === tab.id
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300 dark:text-slate-400 dark:hover:text-slate-300'"
>
<Icon :name="tab.icon" size="16" />
<span>{{ tab.name }}</span>
</button>
</nav>
</div>
<!-- Enhanced Content Sections -->
<div class="p-6">
<!-- Overview Tab -->
<div v-if="activeTab === 'overview'" class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Quick Info -->
<div class="space-y-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">Quick Info</h3>
<div class="space-y-3">
<div class="flex items-center justify-between py-2 border-b border-slate-200 dark:border-slate-600">
<span class="text-sm font-medium text-slate-500 dark:text-slate-400">Operation ID</span>
<code class="text-sm text-slate-900 dark:text-white">{{ selectedEndpoint.operationId }}</code>
</div>
<div class="flex items-center justify-between py-2 border-b border-slate-200 dark:border-slate-600">
<span class="text-sm font-medium text-slate-500 dark:text-slate-400">Tags</span>
<div class="flex space-x-1">
<span
v-for="tag in selectedEndpoint.tags"
:key="tag"
class="px-2 py-1 text-xs bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 rounded"
>
{{ tag }}
</span>
</div>
</div>
<div v-if="selectedEndpoint.security?.length" class="flex items-center justify-between py-2">
<span class="text-sm font-medium text-slate-500 dark:text-slate-400">Authentication</span>
<span class="px-2 py-1 text-xs bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 rounded">Required</span>
</div>
</div>
</div>
<!-- Try It Out -->
<div class="space-y-4">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">Try It Out</h3>
<div class="flex items-center space-x-2">
<button
@click="showAuthModal = true"
class="text-sm flex items-center space-x-1 text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
<Icon name="ic:outline-key" size="14" />
<span>{{ authToken ? 'Change Auth Token' : 'Add Auth Token' }}</span>
</button>
</div>
</div>
<!-- Base URL Selector -->
<div class="space-y-2">
<label class="text-sm font-medium text-slate-700 dark:text-slate-300">Base URL</label>
<div class="flex space-x-2">
<select
v-model="selectedBaseUrl"
class="flex-1 border border-slate-300 dark:border-slate-600 rounded-lg px-3 py-2 bg-white dark:bg-slate-700 text-slate-900 dark:text-white"
>
<option
v-for="baseUrl in availableBaseUrls"
:key="baseUrl.url"
:value="baseUrl.url"
>
{{ baseUrl.name }}: {{ baseUrl.url === 'custom' ? 'Custom URL' : baseUrl.url }}
</option>
</select>
<button
@click="activeTab = 'responses'"
v-if="endpointResponse"
class="px-3 py-2 bg-blue-50 hover:bg-blue-100 text-blue-700 border border-blue-200 rounded-lg transition-colors flex items-center space-x-1"
>
<Icon name="ic:outline-visibility" size="14" />
<span>View Response</span>
</button>
</div>
<!-- Custom URL input -->
<div v-if="selectedBaseUrl === 'custom'" class="mt-2">
<input
v-model="customBaseUrl"
type="text"
placeholder="Enter custom base URL (e.g. https://api.example.com)"
class="w-full border border-slate-300 dark:border-slate-600 rounded-lg px-3 py-2 bg-white dark:bg-slate-700 text-slate-900 dark:text-white"
/>
</div>
<div class="text-xs text-slate-500 dark:text-slate-400 mt-1">
<span>Effective URL: <code class="font-mono">{{ effectiveBaseUrl + selectedEndpoint.path }}</code></span>
</div>
</div>
<div class="p-4 bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
<div v-if="!isTryingEndpoint">
<p class="text-sm text-blue-700 dark:text-blue-300 mb-3">
Test this endpoint directly from the documentation
<span v-if="selectedEndpoint.security?.length && !authToken" class="text-amber-600 dark:text-amber-400 block mt-1">
<Icon name="ic:outline-warning" size="14" class="inline-block mr-1" /> This endpoint requires authentication
</span>
</p>
<button
@click="isTryingEndpoint = true"
class="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center space-x-2"
>
<Icon name="ic:outline-play-arrow" size="16" />
<span>Configure Request</span>
</button>
</div>
<!-- Try It Out Form -->
<div v-else class="space-y-4">
<!-- Path Parameters -->
<div v-if="selectedEndpoint.parameters?.some(p => p.in === 'path')" class="space-y-3">
<h4 class="font-medium text-blue-800 dark:text-blue-300 text-sm">Path Parameters</h4>
<div
v-for="param in selectedEndpoint.parameters.filter(p => p.in === 'path')"
:key="param.name"
class="flex flex-col space-y-1"
>
<label class="text-xs font-medium text-slate-700 dark:text-slate-300">
{{ param.name }}
<span v-if="param.required" class="text-red-500">*</span>
</label>
<input
v-model="paramValues[param.name]"
type="text"
class="w-full px-3 py-1.5 text-sm border border-slate-300 dark:border-slate-600 rounded bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100"
:placeholder="param.description || param.name"
/>
</div>
</div>
<!-- Query Parameters -->
<div v-if="selectedEndpoint.parameters?.some(p => p.in === 'query')" class="space-y-3">
<h4 class="font-medium text-blue-800 dark:text-blue-300 text-sm">Query Parameters</h4>
<div
v-for="param in selectedEndpoint.parameters.filter(p => p.in === 'query')"
:key="param.name"
class="flex flex-col space-y-1"
>
<label class="text-xs font-medium text-slate-700 dark:text-slate-300">
{{ param.name }}
<span v-if="param.required" class="text-red-500">*</span>
</label>
<input
v-model="paramValues[param.name]"
type="text"
class="w-full px-3 py-1.5 text-sm border border-slate-300 dark:border-slate-600 rounded bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100"
:placeholder="param.description || param.name"
/>
</div>
</div>
<!-- Request Body -->
<div v-if="selectedEndpoint.method !== 'GET' && selectedEndpoint.requestBody" class="space-y-2">
<h4 class="font-medium text-blue-800 dark:text-blue-300 text-sm">Request Body</h4>
<textarea
v-model="requestBodyValue"
rows="5"
class="w-full px-3 py-2 text-sm border border-slate-300 dark:border-slate-600 rounded font-mono bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100"
></textarea>
</div>
<!-- Request Actions -->
<div class="flex items-center space-x-2">
<button
@click="executeRequest"
:disabled="isEndpointLoading"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-300 dark:disabled:bg-blue-900 text-white font-medium rounded-lg transition-colors flex items-center space-x-2 flex-1"
>
<span v-if="isEndpointLoading">
<span class="inline-block w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></span>
Sending...
</span>
<span v-else>
<Icon name="ic:outline-send" size="16" class="mr-2" />
Send Request
</span>
</button>
<button
@click="isTryingEndpoint = false"
class="px-4 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600 dark:text-slate-200 font-medium rounded-lg transition-colors"
>
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Parameters Tab -->
<div v-if="activeTab === 'parameters' && selectedEndpoint.parameters?.length" class="space-y-6">
<div class="overflow-hidden border border-slate-200 dark:border-slate-600 rounded-lg">
<div class="bg-slate-50 dark:bg-slate-700/50 px-6 py-3 border-b border-slate-200 dark:border-slate-600">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">Parameters</h3>
</div>
<div class="divide-y divide-slate-200 dark:divide-slate-600">
<div
v-for="param in selectedEndpoint.parameters"
:key="param.name"
class="p-6 hover:bg-slate-50 dark:hover:bg-slate-700/30 transition-colors"
>
<div class="flex items-start justify-between mb-3">
<div class="flex items-center space-x-3">
<code class="text-sm font-mono font-medium text-slate-900 dark:text-white">{{ param.name }}</code>
<span class="px-2 py-1 text-xs font-medium rounded"
:class="param.required
? 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300'
: 'bg-slate-100 text-slate-800 dark:bg-slate-700 dark:text-slate-200'">
{{ param.required ? 'Required' : 'Optional' }}
</span>
<span class="px-2 py-1 text-xs bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 rounded">
{{ param.in }}
</span>
</div>
<span class="text-sm text-slate-500 dark:text-slate-400 font-mono">
{{ param.schema?.type || param.type || 'string' }}
</span>
</div>
<p v-if="param.description" class="text-sm text-slate-600 dark:text-slate-400 leading-relaxed mb-3">
{{ param.description }}
</p>
<div v-if="param.schema?.enum" class="mt-3">
<span class="text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wide">Allowed Values:</span>
<div class="flex flex-wrap gap-1 mt-1">
<code
v-for="value in param.schema.enum"
:key="value"
class="px-2 py-1 text-xs bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded"
>
{{ value }}
</code>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Request Body Tab -->
<div v-if="activeTab === 'body' && selectedEndpoint.requestBody" class="space-y-6">
<div class="border border-slate-200 dark:border-slate-600 rounded-lg overflow-hidden">
<div class="bg-slate-50 dark:bg-slate-700/50 px-6 py-3 border-b border-slate-200 dark:border-slate-600">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">Request Body</h3>
<span v-if="selectedEndpoint.requestBody.required" class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300 rounded">
Required
</span>
</div>
</div>
<div class="p-6">
<p v-if="selectedEndpoint.requestBody.description" class="text-sm text-slate-600 dark:text-slate-400 leading-relaxed mb-4">
{{ selectedEndpoint.requestBody.description }}
</p>
<div v-if="selectedEndpoint.requestBody.content" class="space-y-4">
<div v-for="(content, mediaType) in selectedEndpoint.requestBody.content" :key="mediaType">
<div class="flex items-center space-x-2 mb-3">
<span class="px-3 py-1 text-sm font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 rounded-lg">
{{ mediaType }}
</span>
</div>
<div v-if="content.example" class="relative">
<button
@click="copyToClipboard(JSON.stringify(content.example, null, 2))"
class="absolute top-3 right-3 p-2 bg-slate-800 hover:bg-slate-700 text-white rounded-lg transition-colors z-10"
>
<Icon name="ic:outline-content-copy" size="14" />
</button>
<pre class="bg-slate-900 text-slate-100 p-4 rounded-lg text-sm overflow-x-auto"><code>{{ JSON.stringify(content.example, null, 2) }}</code></pre>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Responses Tab -->
<div v-if="activeTab === 'responses'" class="space-y-6">
<!-- Live Response (when available) -->
<div v-if="endpointResponse" class="border border-slate-200 dark:border-slate-600 rounded-lg overflow-hidden mb-6">
<div class="bg-slate-50 dark:bg-slate-700/50 px-6 py-3 border-b border-slate-200 dark:border-slate-600">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white flex items-center">
<span class="inline-block w-2 h-2 rounded-full bg-emerald-500 mr-2 animate-pulse"></span>
Live Response
</h3>
<div class="flex items-center space-x-3">
<span
class="px-2 py-1 text-xs font-medium rounded"
:class="{
'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300': endpointResponseStatus >= 200 && endpointResponseStatus < 300,
'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300': endpointResponseStatus >= 300 && endpointResponseStatus < 500,
'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300': endpointResponseStatus >= 500 || endpointResponseStatus === 0
}"
>
Status: {{ endpointResponseStatus || 'Error' }}
</span>
<span class="text-xs text-slate-500 dark:text-slate-400">{{ endpointResponseTime }}ms</span>
<button
@click="copyToClipboard(JSON.stringify(endpointResponse, null, 2))"
class="p-1 hover:bg-slate-200 dark:hover:bg-slate-600 rounded transition-colors"
>
<Icon name="ic:outline-content-copy" size="14" class="text-slate-500" />
</button>
</div>
</div>
</div>
<div>
<pre class="bg-slate-900 text-slate-100 p-6 text-sm overflow-x-auto max-h-96"><code>{{ typeof endpointResponse === 'string' ? endpointResponse : JSON.stringify(endpointResponse, null, 2) }}</code></pre>
</div>
</div>
<!-- Documentation Responses -->
<div v-if="Object.keys(selectedEndpoint.responses).length" class="space-y-4">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-2">Documentation Responses</h3>
<div
v-for="(response, status) in selectedEndpoint.responses"
:key="status"
class="border border-slate-200 dark:border-slate-600 rounded-lg overflow-hidden"
>
<div class="bg-slate-50 dark:bg-slate-700/50 px-6 py-3 border-b border-slate-200 dark:border-slate-600">
<div class="flex items-center space-x-3">
<span class="px-3 py-1 text-sm font-medium rounded-lg"
:class="status.startsWith('2') ? 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300' :
status.startsWith('4') ? 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300' :
'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300'">
{{ status }}
</span>
<span class="text-sm font-medium text-slate-900 dark:text-white">{{ response.description }}</span>
</div>
</div>
<div class="p-6">
<div v-if="response.content" class="space-y-4">
<div v-for="(content, mediaType) in response.content" :key="mediaType">
<div class="flex items-center justify-between mb-3">
<span class="px-3 py-1 text-sm font-medium bg-slate-100 text-slate-800 dark:bg-slate-700 dark:text-slate-200 rounded-lg">
{{ mediaType }}
</span>
</div>
<div v-if="content.example" class="relative">
<button
@click="copyToClipboard(JSON.stringify(content.example, null, 2))"
class="absolute top-3 right-3 p-2 bg-slate-800 hover:bg-slate-700 text-white rounded-lg transition-colors z-10"
>
<Icon name="ic:outline-content-copy" size="14" />
</button>
<pre class="bg-slate-900 text-slate-100 p-4 rounded-lg text-sm overflow-x-auto"><code>{{ JSON.stringify(content.example, null, 2) }}</code></pre>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Empty state when no responses -->
<div v-if="!Object.keys(selectedEndpoint.responses).length && !endpointResponse" class="text-center py-12">
<div class="w-12 h-12 mx-auto mb-4 bg-slate-100 dark:bg-slate-700 rounded-full flex items-center justify-center">
<Icon name="ic:outline-info" size="20" class="text-slate-400" />
</div>
<h3 class="text-lg font-medium text-slate-900 dark:text-white mb-2">No responses defined</h3>
<p class="text-slate-600 dark:text-slate-400">This endpoint doesn't have any responses specified in the API specification.</p>
</div>
</div>
<!-- Code Examples Tab -->
<div v-if="activeTab === 'examples'" class="space-y-6">
<div class="border border-slate-200 dark:border-slate-600 rounded-lg overflow-hidden">
<div class="bg-slate-50 dark:bg-slate-700/50 px-6 py-3 border-b border-slate-200 dark:border-slate-600">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">Code Examples</h3>
<div class="flex items-center space-x-2">
<select
v-model="selectedLanguage"
class="px-3 py-1 text-sm border border-slate-200 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option v-for="lang in availableLanguages" :key="lang.value" :value="lang.value">
{{ lang.label }}
</option>
</select>
<button
@click="copyToClipboard(generateCodeExample(selectedEndpoint))"
class="px-3 py-1 text-sm font-medium bg-blue-50 hover:bg-blue-100 text-blue-700 border border-blue-200 rounded-lg transition-colors flex items-center space-x-1"
>
<Icon name="ic:outline-content-copy" size="14" />
<span>Copy</span>
</button>
</div>
</div>
</div>
<div class="relative">
<pre class="bg-slate-900 p-6 text-sm overflow-x-auto"><code :class="getCodeClass(selectedLanguage)" class="syntax-highlighted" v-html="getHighlightedCode"></code></pre>
</div>
</div>
<!-- Example usage instructions -->
<div class="border border-slate-200 dark:border-slate-600 rounded-lg overflow-hidden p-4">
<h4 class="text-sm font-semibold text-slate-900 dark:text-white mb-2">
{{ selectedLanguage === 'curl' ? 'Terminal Usage' : 'Code Usage' }}
</h4>
<div class="text-sm text-slate-600 dark:text-slate-400 space-y-2">
<p v-if="selectedLanguage === 'javascript'">
Copy this code into your JavaScript/Node.js application. You may need to install a fetch polyfill for Node.js.
</p>
<p v-else-if="selectedLanguage === 'python'">
Copy this code into your Python application. You'll need the <code class="px-1 py-0.5 bg-slate-100 dark:bg-slate-700 rounded font-mono text-xs">requests</code> library installed.
</p>
<p v-else-if="selectedLanguage === 'php'">
Copy this code into your PHP application. This uses the cURL extension which is usually enabled by default.
</p>
<p v-else-if="selectedLanguage === 'ruby'">
Copy this code into your Ruby application. This uses Ruby's built-in Net::HTTP library.
</p>
<p v-else-if="selectedLanguage === 'curl'">
Copy this command and run it in your terminal or command prompt.
</p>
</div>
</div>
</div>
<!-- Empty state for tabs with no content -->
<div v-if="(activeTab === 'parameters' && !selectedEndpoint.parameters?.length) ||
(activeTab === 'body' && !selectedEndpoint.requestBody)"
class="text-center py-12">
<div class="w-12 h-12 mx-auto mb-4 bg-slate-100 dark:bg-slate-700 rounded-full flex items-center justify-center">
<Icon name="ic:outline-info" size="20" class="text-slate-400" />
</div>
<h3 class="text-lg font-medium text-slate-900 dark:text-white mb-2">No {{ activeTab }} defined</h3>
<p class="text-slate-600 dark:text-slate-400">This endpoint doesn't have any {{ activeTab }} specified in the API specification.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
/* Enhanced Typography */
.font-inter {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* Syntax highlighting for code blocks */
.syntax-highlighted {
font-family: 'Fira Code', 'Consolas', monospace;
color: #f8f8f2; /* Default text color */
line-height: 1.6;
}
/* Common syntax highlighting classes */
.string { color: #f1fa8c; }
.comment { color: #6272a4; }
.keyword { color: #ff79c6; }
.number { color: #bd93f9; }
.boolean { color: #bd93f9; }
.function { color: #50fa7b; }
.bracket { color: #f8f8f2; }
.param { color: #ffb86c; }
.escape { color: #ff5555; }
.php-tag { color: #ff5555; }
/* Custom scrollbar for code blocks */
pre {
scrollbar-width: thin;
scrollbar-color: rgba(148, 163, 184, 0.3) transparent;
}
pre::-webkit-scrollbar {
height: 8px;
width: 8px;
}
pre::-webkit-scrollbar-track {
background: transparent;
}
pre::-webkit-scrollbar-thumb {
background-color: rgba(148, 163, 184, 0.3);
border-radius: 4px;
}
pre::-webkit-scrollbar-thumb:hover {
background-color: rgba(148, 163, 184, 0.5);
}
/* Smooth transitions */
* {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
/* Enhanced focus states */
button:focus,
input:focus,
select:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* Gradient effects for interactive elements */
.gradient-border {
background: linear-gradient(135deg, rgb(59, 130, 246), rgb(99, 102, 241));
}
</style>