initial commit

This commit is contained in:
2026-05-20 12:05:11 +05:30
commit f4dc2b6a5a
21 changed files with 4822 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
import { useEffect, useRef, useState } from 'react'
import EndpointCard from './EndpointCard'
import { getTopicIcon } from '../lib/icons'
import { BASE_URL } from '../data/topics'
// Strip the documented BASE_URL so fetches are same-origin and get
// proxied by Vite (dev) or Express (prod), which injects the secret header.
function toProxyPath(fullUrl) {
if (fullUrl.startsWith(BASE_URL)) return fullUrl.slice(BASE_URL.length)
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
// Only one endpoint's response is visible at a time. Sending on a new
// endpoint replaces the previous one (and cancels its in-flight fetch).
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 res = await fetch(toProxyPath(composedUrl), {
method: endpoint.method,
headers: { 'Content-Type': 'application/json' },
signal: controller.signal
})
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="max-w-4xl">
<div className="mb-8 flex items-start gap-4">
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-brand-500 to-indigo-600 flex items-center justify-center shadow-glow flex-shrink-0">
<Icon className="text-white w-6 h-6" />
</div>
<div>
<h1 className="text-3xl font-bold text-slate-900 tracking-tight">{topic.name}</h1>
<p className="text-slate-600 mt-2 leading-relaxed">{topic.description}</p>
<div className="mt-3 text-xs text-slate-400 uppercase tracking-wide">
{filtered.length} endpoint{filtered.length === 1 ? '' : 's'}
</div>
</div>
</div>
<div className="space-y-4">
{filtered.length === 0 ? (
<div className="text-center py-16 text-slate-400 text-sm">
No endpoints match "{searchQuery}".
</div>
) : (
filtered.map((e) => {
const isActive = active?.name === e.name
return (
<EndpointCard
key={`${topic.id}/${e.name}`}
endpoint={e}
onSend={handleSend}
result={isActive ? active.result : null}
loading={isActive ? active.loading : false}
/>
)
})
)}
</div>
</div>
)
}