Files
Express_developer_docs/src/components/TopicView.jsx
2026-05-20 17:38:03 +05:30

135 lines
4.4 KiB
JavaScript

import { useEffect, useRef, useState } from 'react'
import EndpointCard from './EndpointCard'
import { getTopicIcon } from '../lib/icons'
import { LEGACY_BASE_URL } from '../data/topics'
const ADMIN_SECRET = 'nearle-admin-secret'
function toProxyPath(fullUrl) {
// Always fetch the full URL directly.
// Legacy (api.workolik.com): CORS open, admin secret injected in headers.
// REST (jupiter.nearle.app): fetched directly, no proxy needed.
return fullUrl
}
export default function TopicView({ topic, searchQuery }) {
const Icon = getTopicIcon(topic.id)
const q = searchQuery.trim().toLowerCase()
const filtered = q
? topic.endpoints.filter((e) =>
e.name.toLowerCase().includes(q) ||
e.url.toLowerCase().includes(q) ||
(e.description || '').toLowerCase().includes(q)
)
: topic.endpoints
const [active, setActive] = useState(null)
const abortRef = useRef(null)
useEffect(() => {
setActive(null)
if (abortRef.current) {
abortRef.current.abort()
abortRef.current = null
}
}, [topic.id])
const handleSend = async (endpoint, composedUrl) => {
if (abortRef.current) abortRef.current.abort()
const controller = new AbortController()
abortRef.current = controller
setActive({ name: endpoint.name, result: null, loading: true })
const start = Date.now()
try {
const headers = { 'Content-Type': 'application/json' }
if (topic.type === 'legacy') headers['x-hasura-admin-secret'] = ADMIN_SECRET
const fetchOptions = {
method: endpoint.method,
headers,
signal: controller.signal
}
if (['POST', 'PUT', 'PATCH'].includes(endpoint.method) && endpoint.body) {
if (typeof endpoint.body === 'string') {
try {
// Re-stringify in case it's a badly formatted JSON string, but mostly it's just raw string
fetchOptions.body = JSON.stringify(JSON.parse(endpoint.body))
} catch {
fetchOptions.body = endpoint.body
}
} else {
fetchOptions.body = JSON.stringify(endpoint.body)
}
}
const res = await fetch(toProxyPath(composedUrl), fetchOptions)
const ms = Date.now() - start
const text = await res.text()
let body
try { body = JSON.parse(text) } catch { body = text }
setActive({
name: endpoint.name,
result: { kind: 'response', status: res.status, ok: res.ok, body, ms },
loading: false
})
} catch (err) {
if (err?.name === 'AbortError') return
setActive({
name: endpoint.name,
result: {
kind: 'network-error',
message: err?.message || String(err),
ms: Date.now() - start
},
loading: false
})
}
}
return (
<div className="pb-24">
<div className="mb-12">
<h1 className="text-4xl lg:text-5xl font-bold text-slate-900 mb-4 tracking-tight">{topic.name}</h1>
<p className="text-lg text-slate-600 max-w-3xl leading-relaxed">{topic.description}</p>
<div className="mt-4 flex items-center gap-3">
<span className={`inline-flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-widest px-3 py-1 rounded-full border ${
topic.type === 'legacy'
? 'bg-brand-50 text-brand-600 border-brand-100'
: 'bg-indigo-50 text-indigo-600 border-indigo-100'
}`}>
<span className="w-1.5 h-1.5 rounded-full bg-current"></span>
{topic.type === 'legacy' ? 'GraphQL API' : 'REST API'}
</span>
<span className="text-sm text-slate-400">{filtered.length} endpoint{filtered.length !== 1 ? 's' : ''}</span>
</div>
</div>
<div className="space-y-4">
{filtered.length === 0 ? (
<div className="text-center py-20 text-slate-400 text-sm bg-white rounded-2xl border border-slate-200/60">
No endpoints match "{searchQuery}".
</div>
) : (
filtered.map((e) => {
const isActive = active?.name === e.name
return (
<EndpointCard
key={`${topic.uniqueId}/${e.name}`}
endpoint={e}
baseUrl={topic.baseUrl}
onSend={handleSend}
result={isActive ? active.result : null}
loading={isActive ? active.loading : false}
/>
)
})
)}
</div>
</div>
)
}