import { useStore } from '@nanostores/react' import { useState } from 'react' import { $apiKeys, createAPIKey, $deleteAPIKey, $creating } from '../stores/apiKeys' import { $webhooks, createWebhook, updateWebhook, $deleteWebhook, testWebhook, fetchWebhookDeliveries } from '../stores/webhooks' import { $billing } from '../stores/billing' import { addToast } from '../stores/app' import { EmptyState, APIPageSkeleton, PageHeader } from '../components/shared' import { Icons } from '../components/shared/Icons' import { Button, Input, Modal, UsageIndicator } from '../components/ui' import type { Webhook, WebhookDelivery, WebhookEvent } from '../types' type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' interface QueryParam { name: string description: string default?: string options?: string[] // For select dropdowns } interface Endpoint { method: HttpMethod path: string description: string queryParams?: QueryParam[] requestBody?: string responseExample: string } const endpoints: Endpoint[] = [ { method: 'GET', path: '/api/v1/posts', description: 'List published posts with pagination', queryParams: [ { name: 'limit', description: 'Results per page (max 100)', default: '20' }, { name: 'offset', description: 'Skip N results', default: '0' }, { name: 'tag', description: 'Filter by tag' }, { name: 'include', description: 'Include extra fields', options: ['', 'content'] }, ], responseExample: `{ "posts": [ { "id": "uuid", "slug": "hello-world", "title": "Hello World", "description": "My first post", "tags": ["intro"], "date": "2024-01-15", "draft": false } ], "total": 42, "limit": 20, "offset": 0 }`, }, { method: 'POST', path: '/api/v1/posts', description: 'Create a new post', requestBody: `{ "title": "My New Post", "slug": "my-new-post", "content": "# Markdown content", "description": "Optional description", "tags": ["tutorial"], "draft": false }`, responseExample: `{ "id": "uuid", "slug": "my-new-post", "title": "My New Post", ... }`, }, { method: 'GET', path: '/api/v1/posts/{slug}', description: 'Get a single post by slug (includes content)', responseExample: `{ "id": "uuid", "slug": "hello-world", "title": "Hello World", "description": "My first post", "content": { "markdown": "# Full markdown...", "html": "

Full markdown...

" }, "tags": ["intro"], "date": "2024-01-15" }`, }, { method: 'PUT', path: '/api/v1/posts/{slug}', description: 'Update an existing post', requestBody: `{ "title": "Updated Title", "content": "# Updated content" }`, responseExample: `{ "id": "uuid", "slug": "hello-world", "title": "Updated Title", ... }`, }, { method: 'DELETE', path: '/api/v1/posts/{slug}', description: 'Delete a post permanently', responseExample: `204 No Content`, }, { method: 'GET', path: '/api/v1/settings', description: 'Get public site configuration', responseExample: `{ "site_name": "My Blog", "site_description": "A developer blog", "author_name": "Jane Doe", "author_bio": "Software engineer", "twitter_handle": "janedoe", "accent_color": "#10b981" }`, }, ] const methodColors: Record = { GET: 'bg-success/15 text-success', POST: 'bg-blue-500/15 text-blue-500', PUT: 'bg-warning/15 text-warning', DELETE: 'bg-danger/15 text-danger', } const webhookEvents: { value: WebhookEvent; label: string }[] = [ { value: 'post.published', label: 'Post Published' }, { value: 'post.updated', label: 'Post Updated' }, { value: 'post.deleted', label: 'Post Deleted' }, ] function EndpointCard({ endpoint, baseUrl, apiKey }: { endpoint: Endpoint; baseUrl: string; apiKey?: string }) { const [expanded, setExpanded] = useState(false) const [slugInput, setSlugInput] = useState('hello-world') const [requestBody, setRequestBody] = useState(endpoint.requestBody || '') const [queryParams, setQueryParams] = useState>(() => { const initial: Record = {} endpoint.queryParams?.forEach(p => { initial[p.name] = p.default || '' }) return initial }) const [response, setResponse] = useState<{ status: number; body: string; time: number } | null>(null) const [loading, setLoading] = useState(false) const basePath = endpoint.path.replace('{slug}', slugInput) const queryString = Object.entries(queryParams) .filter(([_, v]) => v !== '') .map(([k, v]) => `${k}=${encodeURIComponent(v)}`) .join('&') const actualPath = queryString ? `${basePath}?${queryString}` : basePath const fullUrl = `${baseUrl}${actualPath}` const curlExample = endpoint.requestBody ? `curl -X ${endpoint.method} "${fullUrl}" \\ -H "Authorization: Bearer ${apiKey || 'YOUR_API_KEY'}" \\ -H "Content-Type: application/json" \\ -d '${endpoint.requestBody.replace(/\n\s*/g, ' ')}'` : `curl -X ${endpoint.method} "${fullUrl}" \\ -H "Authorization: Bearer ${apiKey || 'YOUR_API_KEY'}"` const copyCode = (code: string) => { navigator.clipboard.writeText(code) addToast('Copied to clipboard', 'success') } const updateQueryParam = (name: string, value: string) => { setQueryParams(prev => ({ ...prev, [name]: value })) } const sendRequest = async () => { setLoading(true) setResponse(null) const start = Date.now() try { const headers: Record = { 'Content-Type': 'application/json', } if (apiKey) { headers['Authorization'] = `Bearer ${apiKey}` } const res = await fetch(actualPath, { method: endpoint.method, headers, body: endpoint.requestBody ? requestBody : undefined, }) const time = Date.now() - start let body: string try { const json = await res.json() body = JSON.stringify(json, null, 2) } catch { body = res.status === 204 ? '(No Content)' : await res.text() } setResponse({ status: res.status, body, time }) } catch (err) { setResponse({ status: 0, body: `Error: ${err instanceof Error ? err.message : 'Request failed'}`, time: Date.now() - start }) } finally { setLoading(false) } } return (
{expanded && (

{endpoint.description}

{endpoint.path.includes('{slug}') && (
)} {endpoint.queryParams && endpoint.queryParams.length > 0 && (
Query Parameters
{endpoint.queryParams.map(param => (
{param.options ? ( ) : ( updateQueryParam(param.name, v)} className="max-w-32 text-sm" placeholder={param.default || ''} /> )} {param.description}
))}
)} {endpoint.requestBody && (
Request Body