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 (

{topic.name}

{topic.description}

{topic.type === 'legacy' ? 'GraphQL API' : 'REST API'} {filtered.length} endpoint{filtered.length !== 1 ? 's' : ''}
{filtered.length === 0 ? (
No endpoints match "{searchQuery}".
) : ( filtered.map((e) => { const isActive = active?.name === e.name return ( ) }) )}
) }