259 lines
9.7 KiB
TypeScript
259 lines
9.7 KiB
TypeScript
import { useState } from 'react'
|
|
import { getAuthToken } from '@/services/auth'
|
|
|
|
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:4000'
|
|
|
|
export function DemoRoute() {
|
|
const [method, setMethod] = useState<string>('GET')
|
|
const [endpoint, setEndpoint] = useState<string>('/api/test')
|
|
const [headers, setHeaders] = useState<string>('{}')
|
|
const [body, setBody] = useState<string>('{}')
|
|
const [response, setResponse] = useState<any>(null)
|
|
const [error, setError] = useState<string>('')
|
|
const [loading, setLoading] = useState(false)
|
|
const [history, setHistory] = useState<Array<{ method: string; endpoint: string; timestamp: string }>>([])
|
|
|
|
const handleSubmit = async () => {
|
|
setLoading(true)
|
|
setError('')
|
|
setResponse(null)
|
|
|
|
try {
|
|
const token = await getAuthToken()
|
|
const url = `${API_BASE_URL}${endpoint}`
|
|
|
|
let parsedHeaders: Record<string, string> = {}
|
|
try {
|
|
parsedHeaders = JSON.parse(headers)
|
|
} catch {
|
|
throw new Error('Invalid headers JSON')
|
|
}
|
|
|
|
const fetchOptions: RequestInit = {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(token && { Authorization: `Bearer ${token}` }),
|
|
...parsedHeaders,
|
|
},
|
|
}
|
|
|
|
if (method !== 'GET' && method !== 'HEAD') {
|
|
try {
|
|
const parsedBody = JSON.parse(body)
|
|
fetchOptions.body = JSON.stringify(parsedBody)
|
|
} catch {
|
|
throw new Error('Invalid body JSON')
|
|
}
|
|
}
|
|
|
|
const startTime = performance.now()
|
|
const res = await fetch(url, fetchOptions)
|
|
const endTime = performance.now()
|
|
const duration = Math.round(endTime - startTime)
|
|
|
|
let data
|
|
const contentType = res.headers.get('content-type')
|
|
if (contentType && contentType.includes('application/json')) {
|
|
data = await res.json()
|
|
} else {
|
|
data = await res.text()
|
|
}
|
|
|
|
setResponse({
|
|
status: res.status,
|
|
statusText: res.statusText,
|
|
duration: `${duration}ms`,
|
|
headers: Object.fromEntries(res.headers.entries()),
|
|
data,
|
|
})
|
|
|
|
// Add to history
|
|
setHistory(prev => [
|
|
{ method, endpoint, timestamp: new Date().toLocaleTimeString() },
|
|
...prev.slice(0, 9), // Keep last 10
|
|
])
|
|
} catch (err: any) {
|
|
setError(err.message || 'An error occurred')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const loadFromHistory = (item: { method: string; endpoint: string }) => {
|
|
setMethod(item.method)
|
|
setEndpoint(item.endpoint)
|
|
}
|
|
|
|
const getStatusColor = (status: number) => {
|
|
if (status >= 200 && status < 300) return 'bg-green-500'
|
|
if (status >= 300 && status < 400) return 'bg-yellow-500'
|
|
if (status >= 400) return 'bg-red-500'
|
|
return 'bg-gray-500'
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-6xl mx-auto p-6 space-y-6">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h1 className="text-3xl font-bold text-gray-900">API Demo & Testing</h1>
|
|
<span className="px-3 py-1 bg-gray-200 rounded-full text-sm text-gray-700">
|
|
{API_BASE_URL}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* Request Panel */}
|
|
<div className="lg:col-span-2 bg-white rounded-lg shadow-md p-6">
|
|
<h2 className="text-xl font-semibold mb-4">Request</h2>
|
|
<div className="space-y-4">
|
|
<div className="flex gap-2">
|
|
<select
|
|
value={method}
|
|
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setMethod(e.target.value)}
|
|
className="px-4 py-2 border rounded-md bg-white w-32"
|
|
>
|
|
<option value="GET">GET</option>
|
|
<option value="POST">POST</option>
|
|
<option value="PUT">PUT</option>
|
|
<option value="PATCH">PATCH</option>
|
|
<option value="DELETE">DELETE</option>
|
|
</select>
|
|
<input
|
|
type="text"
|
|
placeholder="/api/endpoint"
|
|
value={endpoint}
|
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEndpoint(e.target.value)}
|
|
className="flex-1 px-4 py-2 border rounded-md"
|
|
/>
|
|
<button
|
|
onClick={handleSubmit}
|
|
disabled={loading}
|
|
className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-blue-400 transition-colors"
|
|
>
|
|
{loading ? 'Sending...' : 'Send'}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="block text-sm font-medium text-gray-700">Headers (JSON)</label>
|
|
<textarea
|
|
value={headers}
|
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setHeaders(e.target.value)}
|
|
placeholder='{"Custom-Header": "value"}'
|
|
className="w-full px-4 py-2 border rounded-md font-mono text-sm min-h-[80px]"
|
|
/>
|
|
</div>
|
|
|
|
{method !== 'GET' && method !== 'HEAD' && (
|
|
<div className="space-y-2">
|
|
<label className="block text-sm font-medium text-gray-700">Body (JSON)</label>
|
|
<textarea
|
|
value={body}
|
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setBody(e.target.value)}
|
|
placeholder='{"key": "value"}'
|
|
className="w-full px-4 py-2 border rounded-md font-mono text-sm min-h-[120px]"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* History Panel */}
|
|
<div className="bg-white rounded-lg shadow-md p-6">
|
|
<h2 className="text-xl font-semibold mb-4">History</h2>
|
|
<div className="space-y-2 max-h-[400px] overflow-y-auto">
|
|
{history.length === 0 ? (
|
|
<p className="text-gray-500 text-sm">No requests yet</p>
|
|
) : (
|
|
history.map((item, index) => (
|
|
<button
|
|
key={index}
|
|
onClick={() => loadFromHistory(item)}
|
|
className="w-full text-left p-3 rounded hover:bg-gray-100 transition-colors text-sm border"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
|
|
item.method === 'GET' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'
|
|
}`}>
|
|
{item.method}
|
|
</span>
|
|
<span className="truncate flex-1 text-gray-900">{item.endpoint}</span>
|
|
</div>
|
|
<div className="text-xs text-gray-500 mt-1">
|
|
{item.timestamp}
|
|
</div>
|
|
</button>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Response Panel */}
|
|
<div className="lg:col-span-3 bg-white rounded-lg shadow-md p-6">
|
|
<h2 className="text-xl font-semibold mb-4">Response</h2>
|
|
{error ? (
|
|
<div className="p-4 bg-red-50 border border-red-200 rounded-md text-red-700">
|
|
<p className="font-medium">Error</p>
|
|
<p className="text-sm">{error}</p>
|
|
</div>
|
|
) : response ? (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-4">
|
|
<span className={`px-3 py-1 rounded-full text-white text-sm font-medium ${getStatusColor(response.status)}`}>
|
|
{response.status} {response.statusText}
|
|
</span>
|
|
<span className="px-3 py-1 border rounded-full text-sm">
|
|
{response.duration}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="block text-sm font-medium text-gray-700">Response Headers</label>
|
|
<pre className="bg-gray-100 p-3 rounded-md overflow-x-auto text-xs">
|
|
{JSON.stringify(response.headers, null, 2)}
|
|
</pre>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="block text-sm font-medium text-gray-700">Response Body</label>
|
|
<pre className="bg-gray-100 p-3 rounded-md overflow-x-auto text-xs">
|
|
{typeof response.data === 'string'
|
|
? response.data
|
|
: JSON.stringify(response.data, null, 2)}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-12 text-gray-500">
|
|
<p>Send a request to see the response</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Links */}
|
|
<div className="bg-white rounded-lg shadow-md p-6">
|
|
<h2 className="text-xl font-semibold mb-4">Quick Test Endpoints</h2>
|
|
<div className="flex flex-wrap gap-2">
|
|
{[
|
|
{ method: 'GET', endpoint: '/trpc/user.banner.getActiveBanners' },
|
|
{ method: 'GET', endpoint: '/trpc/user.product.getAllProducts' },
|
|
{ method: 'GET', endpoint: '/trpc/user.user.getCurrentUser' },
|
|
{ method: 'POST', endpoint: '/trpc/user.auth.login' },
|
|
].map((item, index) => (
|
|
<button
|
|
key={index}
|
|
onClick={() => loadFromHistory(item)}
|
|
className="px-4 py-2 border rounded-md hover:bg-gray-50 transition-colors text-sm"
|
|
>
|
|
<span className="mr-2 px-2 py-0.5 bg-gray-200 rounded text-xs">
|
|
{item.method}
|
|
</span>
|
|
{item.endpoint}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|