'use client'; import { Check, Copy, Play } from 'lucide-react'; import { useMemo, useState } from 'react'; import { t } from '@/i18n'; import { rawRequest } from '@/lib/client-api-platform'; import { cn } from '@/lib/cn'; import { compact } from '@/lib/format'; export interface ExplorerParam { name: string; /** `path` params are substituted in the template; `query` params appended. */ in: 'path' | 'query'; description: string; example?: string; required?: boolean; options?: string[]; } export interface ExplorerEndpoint { id: string; group: string; /** e.g. "/countries/{id}/series/{indicator}" */ template: string; summary: string; params: ExplorerParam[]; } const MAX_SHOW = 40_000; function buildPath(ep: ExplorerEndpoint, values: Record): string { let path = ep.template; const q = new URLSearchParams(); for (const p of ep.params) { const v = (values[p.name] ?? p.example ?? '').trim(); if (p.in === 'path') path = path.replace(`{${p.name}}`, encodeURIComponent(v || p.example || '')); else if (v) q.append(p.name, v); } const s = q.toString(); return `/api/v1${path}${s ? `?${s}` : ''}`; } function snippets(url: string): Record<'curl' | 'js' | 'py', string> { return { curl: `curl -s "${url}" | jq .`, js: `const res = await fetch("${url}", { headers: { accept: "application/json" } });\nconst data = await res.json();\nconsole.log(data.meta, data);`, py: `import requests\n\nr = requests.get("${url}", headers={"accept": "application/json"}, timeout=30)\nr.raise_for_status()\ndata = r.json()\nprint(data["meta"], list(data)[:8])`, }; } /** * Interactive endpoint explorer: pick an endpoint, fill typed parameters, run it against the same-origin API, * see status/latency/size and the pretty JSON (capped), and copy the request as curl / JavaScript / Python. */ export function EndpointExplorer({ endpoints, base }: { endpoints: ExplorerEndpoint[]; base: string }) { const [id, setId] = useState(endpoints[0]?.id ?? ''); const ep = useMemo(() => endpoints.find((e) => e.id === id) ?? endpoints[0]!, [endpoints, id]); const [values, setValues] = useState>({}); const [lang, setLang] = useState<'curl' | 'js' | 'py'>('curl'); const [copied, setCopied] = useState(false); const [state, setState] = useState<'idle' | 'running' | 'done' | 'error'>('idle'); const [result, setResult] = useState<{ status: number; ms: number; bytes: number; text: string } | null>(null); const [error, setError] = useState(null); const path = buildPath(ep, values); const url = `${base}${path}`; const snip = snippets(url); const groups = Array.from(new Set(endpoints.map((e) => e.group))); const run = async () => { setState('running'); setError(null); try { const r = await rawRequest(path); const pretty = r.json != null ? JSON.stringify(r.json, null, 2) : r.text; setResult({ status: r.status, ms: r.ms, bytes: r.bytes, text: pretty }); setState('done'); } catch (e) { setError((e as Error).message); setState('error'); } }; const copy = async () => { try { await navigator.clipboard.writeText(snip[lang]); setCopied(true); setTimeout(() => setCopied(false), 1600); } catch { /* ignore */ } }; const shown = result ? (result.text.length > MAX_SHOW ? result.text.slice(0, MAX_SHOW) : result.text) : ''; return (

{ep.summary}

{ep.params.length ? (
{t('apiPage.explorer.params')}
    {ep.params.map((p) => (
  • {p.options ? ( ) : ( setValues((v) => ({ ...v, [p.name]: e.target.value }))} className="h-11 w-full rounded-sm border border-rule bg-surface px-2 font-mono text-sm text-ink outline-none placeholder:text-ink-3 focus:border-accent md:h-9" /> )}

    {p.description}

  • ))}
) : null}
{t('apiPage.explorer.request')}
{(['curl', 'js', 'py'] as const).map((l) => ( ))}
            {snip[lang]}
          
{t('apiPage.explorer.response')}
{state === 'error' ?

{t('apiPage.explorer.error', { msg: error ?? '' })}

: null} {result ? (
{t('apiPage.explorer.status', { status: result.status, ms: result.ms, size: `${compact(result.bytes)}B` })} {result.text.length > MAX_SHOW ? · {t('apiPage.explorer.truncated', { n: compact(MAX_SHOW) })} : null}
) : null}
          {shown || `GET ${path}`}
        
); }