@barodoc/theme-docs 5.0.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -2
- package/src/components/Contributors.astro +71 -0
- package/src/components/KeyboardShortcuts.astro +108 -0
- package/src/components/VersionSwitcher.tsx +79 -0
- package/src/components/index.ts +6 -0
- package/src/components/mdx/ApiEndpoint.tsx +35 -0
- package/src/components/mdx/ApiPlayground.tsx +789 -0
- package/src/components/mdx/ImageZoom.tsx +35 -0
- package/src/components/mdx/Video.tsx +71 -0
- package/src/index.ts +27 -1
- package/src/layouts/BaseLayout.astro +3 -0
- package/src/layouts/BlogLayout.astro +93 -0
- package/src/layouts/DocsLayout.astro +33 -1
- package/src/pages/blog/[...slug].astro +39 -0
- package/src/pages/blog/index.astro +92 -0
- package/src/pages/changelog/index.astro +72 -0
- package/src/pages/docs/[...slug].astro +4 -0
- package/src/styles/global.css +1436 -0
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
|
|
4
|
+
interface ParamDef {
|
|
5
|
+
name: string;
|
|
6
|
+
in: "query" | "path" | "header" | "body";
|
|
7
|
+
type?: string;
|
|
8
|
+
required?: boolean;
|
|
9
|
+
defaultValue?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
enum?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ApiPlaygroundProps {
|
|
15
|
+
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
16
|
+
url: string;
|
|
17
|
+
params?: ParamDef[];
|
|
18
|
+
body?: string;
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface HistoryEntry {
|
|
24
|
+
id: number;
|
|
25
|
+
method: string;
|
|
26
|
+
url: string;
|
|
27
|
+
status: number;
|
|
28
|
+
statusText: string;
|
|
29
|
+
time: number;
|
|
30
|
+
size: number;
|
|
31
|
+
body: string;
|
|
32
|
+
headers: [string, string][];
|
|
33
|
+
timestamp: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const METHOD_COLORS: Record<string, string> = {
|
|
37
|
+
GET: "bd-method-get",
|
|
38
|
+
POST: "bd-method-post",
|
|
39
|
+
PUT: "bd-method-put",
|
|
40
|
+
PATCH: "bd-method-patch",
|
|
41
|
+
DELETE: "bd-method-delete",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type SnippetLang = "curl" | "javascript" | "python" | "node";
|
|
45
|
+
type ResponseTab = "body" | "headers" | "code";
|
|
46
|
+
|
|
47
|
+
const SNIPPET_LABELS: Record<SnippetLang, string> = {
|
|
48
|
+
curl: "cURL",
|
|
49
|
+
javascript: "JavaScript",
|
|
50
|
+
python: "Python",
|
|
51
|
+
node: "Node.js",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const DB_NAME = "barodoc-api-playground";
|
|
55
|
+
const DB_VERSION = 1;
|
|
56
|
+
const STORE_NAME = "history";
|
|
57
|
+
const MAX_HISTORY = 5;
|
|
58
|
+
|
|
59
|
+
function openDb(): Promise<IDBDatabase> {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
62
|
+
req.onupgradeneeded = () => {
|
|
63
|
+
const db = req.result;
|
|
64
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
65
|
+
db.createObjectStore(STORE_NAME, { keyPath: "id", autoIncrement: true });
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
req.onsuccess = () => resolve(req.result);
|
|
69
|
+
req.onerror = () => reject(req.error);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function loadHistory(): Promise<HistoryEntry[]> {
|
|
74
|
+
try {
|
|
75
|
+
const db = await openDb();
|
|
76
|
+
return new Promise((resolve) => {
|
|
77
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
78
|
+
const store = tx.objectStore(STORE_NAME);
|
|
79
|
+
const req = store.getAll();
|
|
80
|
+
req.onsuccess = () => {
|
|
81
|
+
const items = (req.result as HistoryEntry[]).sort((a, b) => b.timestamp - a.timestamp);
|
|
82
|
+
resolve(items.slice(0, MAX_HISTORY));
|
|
83
|
+
};
|
|
84
|
+
req.onerror = () => resolve([]);
|
|
85
|
+
});
|
|
86
|
+
} catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function saveHistory(entry: Omit<HistoryEntry, "id">): Promise<void> {
|
|
92
|
+
try {
|
|
93
|
+
const db = await openDb();
|
|
94
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
95
|
+
const store = tx.objectStore(STORE_NAME);
|
|
96
|
+
store.add(entry);
|
|
97
|
+
|
|
98
|
+
const allReq = store.getAll();
|
|
99
|
+
allReq.onsuccess = () => {
|
|
100
|
+
const items = (allReq.result as HistoryEntry[]).sort((a, b) => b.timestamp - a.timestamp);
|
|
101
|
+
const toDelete = items.slice(MAX_HISTORY);
|
|
102
|
+
for (const item of toDelete) {
|
|
103
|
+
store.delete(item.id);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
} catch {
|
|
107
|
+
// IndexedDB unavailable — silent fail
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function generateSnippet(
|
|
112
|
+
lang: SnippetLang,
|
|
113
|
+
method: string,
|
|
114
|
+
url: string,
|
|
115
|
+
headers: Record<string, string>,
|
|
116
|
+
body?: string,
|
|
117
|
+
): string {
|
|
118
|
+
switch (lang) {
|
|
119
|
+
case "curl": {
|
|
120
|
+
const parts = [`curl -X ${method} '${url}'`];
|
|
121
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
122
|
+
parts.push(` -H '${k}: ${v}'`);
|
|
123
|
+
}
|
|
124
|
+
if (body) parts.push(` -d '${body}'`);
|
|
125
|
+
return parts.join(" \\\n");
|
|
126
|
+
}
|
|
127
|
+
case "javascript": {
|
|
128
|
+
const opts: string[] = [` method: "${method}"`];
|
|
129
|
+
if (Object.keys(headers).length > 0) {
|
|
130
|
+
opts.push(` headers: ${JSON.stringify(headers, null, 4).replace(/\n/g, "\n ")}`);
|
|
131
|
+
}
|
|
132
|
+
if (body) opts.push(` body: JSON.stringify(${body})`);
|
|
133
|
+
return `const response = await fetch("${url}", {\n${opts.join(",\n")}\n});\nconst data = await response.json();\nconsole.log(data);`;
|
|
134
|
+
}
|
|
135
|
+
case "python": {
|
|
136
|
+
const lines = ["import requests", ""];
|
|
137
|
+
if (body) {
|
|
138
|
+
lines.push(`response = requests.${method.toLowerCase()}(`);
|
|
139
|
+
lines.push(` "${url}",`);
|
|
140
|
+
if (Object.keys(headers).length > 0) {
|
|
141
|
+
lines.push(` headers=${pythonDict(headers)},`);
|
|
142
|
+
}
|
|
143
|
+
lines.push(` json=${body}`);
|
|
144
|
+
lines.push(")");
|
|
145
|
+
} else {
|
|
146
|
+
lines.push(`response = requests.${method.toLowerCase()}(`);
|
|
147
|
+
lines.push(` "${url}"${Object.keys(headers).length > 0 ? "," : ""}`);
|
|
148
|
+
if (Object.keys(headers).length > 0) {
|
|
149
|
+
lines.push(` headers=${pythonDict(headers)}`);
|
|
150
|
+
}
|
|
151
|
+
lines.push(")");
|
|
152
|
+
}
|
|
153
|
+
lines.push("print(response.json())");
|
|
154
|
+
return lines.join("\n");
|
|
155
|
+
}
|
|
156
|
+
case "node": {
|
|
157
|
+
const lines = ['const axios = require("axios");', ""];
|
|
158
|
+
const config: string[] = [];
|
|
159
|
+
if (Object.keys(headers).length > 0) {
|
|
160
|
+
config.push(` headers: ${JSON.stringify(headers, null, 4).replace(/\n/g, "\n ")}`);
|
|
161
|
+
}
|
|
162
|
+
if (body) config.push(` data: ${body}`);
|
|
163
|
+
lines.push(`const { data } = await axios({`);
|
|
164
|
+
lines.push(` method: "${method}",`);
|
|
165
|
+
lines.push(` url: "${url}",`);
|
|
166
|
+
if (config.length > 0) lines.push(config.join(",\n") + ",");
|
|
167
|
+
lines.push("});");
|
|
168
|
+
lines.push("console.log(data);");
|
|
169
|
+
return lines.join("\n");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function pythonDict(obj: Record<string, string>): string {
|
|
175
|
+
const entries = Object.entries(obj).map(([k, v]) => `"${k}": "${v}"`);
|
|
176
|
+
if (entries.length <= 2) return `{${entries.join(", ")}}`;
|
|
177
|
+
return `{\n ${entries.join(",\n ")}\n }`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function highlightJson(text: string): React.ReactNode[] {
|
|
181
|
+
const withValues = text.replace(
|
|
182
|
+
/("(?:[^"\\]|\\.)*")(\s*:)?|(\b(?:true|false|null)\b)|(\b-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)/g,
|
|
183
|
+
(match, str, colon, bool, num) => {
|
|
184
|
+
if (str && colon) return `<k>${str}</k>${colon}`;
|
|
185
|
+
if (str) return `<s>${str}</s>`;
|
|
186
|
+
if (bool) return `<b>${bool}</b>`;
|
|
187
|
+
if (num) return `<n>${num}</n>`;
|
|
188
|
+
return match;
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const parts = withValues.split(/(<[ksnb]>.*?<\/[ksnb]>)/g);
|
|
193
|
+
return parts.map((part, i) => {
|
|
194
|
+
const keyM = part.match(/^<k>(.*)<\/k>$/);
|
|
195
|
+
if (keyM) return <span key={i} className="bd-json-key">{keyM[1]}</span>;
|
|
196
|
+
const strM = part.match(/^<s>(.*)<\/s>$/);
|
|
197
|
+
if (strM) return <span key={i} className="bd-json-str">{strM[1]}</span>;
|
|
198
|
+
const boolM = part.match(/^<b>(.*)<\/b>$/);
|
|
199
|
+
if (boolM) return <span key={i} className="bd-json-bool">{boolM[1]}</span>;
|
|
200
|
+
const numM = part.match(/^<n>(.*)<\/n>$/);
|
|
201
|
+
if (numM) return <span key={i} className="bd-json-num">{numM[1]}</span>;
|
|
202
|
+
return <React.Fragment key={i}>{part}</React.Fragment>;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function formatSize(bytes: number): string {
|
|
207
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
208
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
209
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function CopyButton({ text }: { text: string }) {
|
|
213
|
+
const [copied, setCopied] = React.useState(false);
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<button
|
|
217
|
+
className="bd-pg-copy"
|
|
218
|
+
onClick={() => {
|
|
219
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
220
|
+
setCopied(true);
|
|
221
|
+
setTimeout(() => setCopied(false), 2000);
|
|
222
|
+
});
|
|
223
|
+
}}
|
|
224
|
+
title="Copy"
|
|
225
|
+
>
|
|
226
|
+
{copied ? (
|
|
227
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
228
|
+
<polyline points="20 6 9 17 4 12" />
|
|
229
|
+
</svg>
|
|
230
|
+
) : (
|
|
231
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
232
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
233
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
234
|
+
</svg>
|
|
235
|
+
)}
|
|
236
|
+
</button>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function ApiPlayground({
|
|
241
|
+
method = "GET",
|
|
242
|
+
url,
|
|
243
|
+
params = [],
|
|
244
|
+
body: initialBody,
|
|
245
|
+
headers: initialHeaders = {},
|
|
246
|
+
className,
|
|
247
|
+
}: ApiPlaygroundProps) {
|
|
248
|
+
const [paramValues, setParamValues] = React.useState<Record<string, string>>(() => {
|
|
249
|
+
const vals: Record<string, string> = {};
|
|
250
|
+
for (const p of params) vals[p.name] = p.defaultValue || "";
|
|
251
|
+
return vals;
|
|
252
|
+
});
|
|
253
|
+
const [bodyValue, setBodyValue] = React.useState(initialBody || "");
|
|
254
|
+
const [bodyError, setBodyError] = React.useState<string | null>(null);
|
|
255
|
+
const [response, setResponse] = React.useState<{
|
|
256
|
+
status: number;
|
|
257
|
+
statusText: string;
|
|
258
|
+
body: string;
|
|
259
|
+
headers: [string, string][];
|
|
260
|
+
time: number;
|
|
261
|
+
size: number;
|
|
262
|
+
} | null>(null);
|
|
263
|
+
const [loading, setLoading] = React.useState(false);
|
|
264
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
265
|
+
const [responseTab, setResponseTab] = React.useState<ResponseTab>("body");
|
|
266
|
+
const [snippetLang, setSnippetLang] = React.useState<SnippetLang>("curl");
|
|
267
|
+
const [showAuth, setShowAuth] = React.useState(false);
|
|
268
|
+
const [authType, setAuthType] = React.useState<"bearer" | "apikey" | "basic">("bearer");
|
|
269
|
+
const [bearerToken, setBearerToken] = React.useState(() =>
|
|
270
|
+
typeof window !== "undefined" ? localStorage.getItem("bd-api-bearer") || "" : "",
|
|
271
|
+
);
|
|
272
|
+
const [apiKeyName, setApiKeyName] = React.useState(() =>
|
|
273
|
+
typeof window !== "undefined" ? localStorage.getItem("bd-api-keyname") || "X-API-Key" : "X-API-Key",
|
|
274
|
+
);
|
|
275
|
+
const [apiKeyValue, setApiKeyValue] = React.useState(() =>
|
|
276
|
+
typeof window !== "undefined" ? localStorage.getItem("bd-api-keyvalue") || "" : "",
|
|
277
|
+
);
|
|
278
|
+
const [basicUser, setBasicUser] = React.useState(() =>
|
|
279
|
+
typeof window !== "undefined" ? localStorage.getItem("bd-api-basic-user") || "" : "",
|
|
280
|
+
);
|
|
281
|
+
const [basicPass, setBasicPass] = React.useState(() =>
|
|
282
|
+
typeof window !== "undefined" ? localStorage.getItem("bd-api-basic-pass") || "" : "",
|
|
283
|
+
);
|
|
284
|
+
const [history, setHistory] = React.useState<HistoryEntry[]>([]);
|
|
285
|
+
const [showHistory, setShowHistory] = React.useState(false);
|
|
286
|
+
const [historyView, setHistoryView] = React.useState<HistoryEntry | null>(null);
|
|
287
|
+
|
|
288
|
+
React.useEffect(() => {
|
|
289
|
+
if (typeof window === "undefined") return;
|
|
290
|
+
localStorage.setItem("bd-api-bearer", bearerToken);
|
|
291
|
+
localStorage.setItem("bd-api-keyname", apiKeyName);
|
|
292
|
+
localStorage.setItem("bd-api-keyvalue", apiKeyValue);
|
|
293
|
+
localStorage.setItem("bd-api-basic-user", basicUser);
|
|
294
|
+
localStorage.setItem("bd-api-basic-pass", basicPass);
|
|
295
|
+
}, [bearerToken, apiKeyName, apiKeyValue, basicUser, basicPass]);
|
|
296
|
+
|
|
297
|
+
React.useEffect(() => {
|
|
298
|
+
loadHistory().then(setHistory);
|
|
299
|
+
}, []);
|
|
300
|
+
|
|
301
|
+
function buildUrl(): string {
|
|
302
|
+
let finalUrl = url;
|
|
303
|
+
const queryParams = new URLSearchParams();
|
|
304
|
+
for (const p of params) {
|
|
305
|
+
const val = paramValues[p.name] || "";
|
|
306
|
+
if (p.in === "path") {
|
|
307
|
+
finalUrl = finalUrl.replace(`{${p.name}}`, encodeURIComponent(val));
|
|
308
|
+
} else if (p.in === "query" && val) {
|
|
309
|
+
queryParams.set(p.name, val);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const qs = queryParams.toString();
|
|
313
|
+
return qs ? `${finalUrl}?${qs}` : finalUrl;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function buildLiveUrl(): string {
|
|
317
|
+
let finalUrl = url;
|
|
318
|
+
for (const p of params) {
|
|
319
|
+
const val = paramValues[p.name];
|
|
320
|
+
if (p.in === "path") {
|
|
321
|
+
finalUrl = val
|
|
322
|
+
? finalUrl.replace(`{${p.name}}`, val)
|
|
323
|
+
: finalUrl;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const queryParts: string[] = [];
|
|
327
|
+
for (const p of params) {
|
|
328
|
+
if (p.in === "query" && paramValues[p.name]) {
|
|
329
|
+
queryParts.push(`${p.name}=${paramValues[p.name]}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return queryParts.length > 0 ? `${finalUrl}?${queryParts.join("&")}` : finalUrl;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function getRequestHeaders(): Record<string, string> {
|
|
336
|
+
const h: Record<string, string> = { ...initialHeaders };
|
|
337
|
+
for (const p of params) {
|
|
338
|
+
if (p.in === "header" && paramValues[p.name]) {
|
|
339
|
+
h[p.name] = paramValues[p.name];
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (bearerToken && authType === "bearer") {
|
|
343
|
+
h["Authorization"] = `Bearer ${bearerToken}`;
|
|
344
|
+
}
|
|
345
|
+
if (apiKeyValue && authType === "apikey") {
|
|
346
|
+
h[apiKeyName] = apiKeyValue;
|
|
347
|
+
}
|
|
348
|
+
if (basicUser && authType === "basic") {
|
|
349
|
+
h["Authorization"] = `Basic ${btoa(`${basicUser}:${basicPass}`)}`;
|
|
350
|
+
}
|
|
351
|
+
const hasBody = ["POST", "PUT", "PATCH"].includes(method) && bodyValue;
|
|
352
|
+
if (hasBody && !h["Content-Type"]) {
|
|
353
|
+
h["Content-Type"] = "application/json";
|
|
354
|
+
}
|
|
355
|
+
return h;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function sendRequest() {
|
|
359
|
+
const hasBody = ["POST", "PUT", "PATCH"].includes(method) && bodyValue;
|
|
360
|
+
|
|
361
|
+
if (hasBody) {
|
|
362
|
+
try {
|
|
363
|
+
JSON.parse(bodyValue);
|
|
364
|
+
setBodyError(null);
|
|
365
|
+
} catch (e) {
|
|
366
|
+
setBodyError(e instanceof Error ? e.message : "Invalid JSON");
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
setLoading(true);
|
|
372
|
+
setError(null);
|
|
373
|
+
setResponse(null);
|
|
374
|
+
setHistoryView(null);
|
|
375
|
+
setResponseTab("body");
|
|
376
|
+
|
|
377
|
+
const start = performance.now();
|
|
378
|
+
const reqUrl = buildUrl();
|
|
379
|
+
const headers = getRequestHeaders();
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
const res = await fetch(reqUrl, {
|
|
383
|
+
method,
|
|
384
|
+
headers,
|
|
385
|
+
body: hasBody ? bodyValue : undefined,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const elapsed = Math.round(performance.now() - start);
|
|
389
|
+
const rawText = await res.text();
|
|
390
|
+
const size = new Blob([rawText]).size;
|
|
391
|
+
let text: string;
|
|
392
|
+
try {
|
|
393
|
+
const json = JSON.parse(rawText);
|
|
394
|
+
text = JSON.stringify(json, null, 2);
|
|
395
|
+
} catch {
|
|
396
|
+
text = rawText;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const resHeaders: [string, string][] = [];
|
|
400
|
+
res.headers.forEach((v, k) => resHeaders.push([k, v]));
|
|
401
|
+
|
|
402
|
+
const resObj = { status: res.status, statusText: res.statusText, body: text, headers: resHeaders, time: elapsed, size };
|
|
403
|
+
setResponse(resObj);
|
|
404
|
+
|
|
405
|
+
const historyEntry: Omit<HistoryEntry, "id"> = {
|
|
406
|
+
method,
|
|
407
|
+
url: reqUrl,
|
|
408
|
+
status: res.status,
|
|
409
|
+
statusText: res.statusText,
|
|
410
|
+
time: elapsed,
|
|
411
|
+
size,
|
|
412
|
+
body: text,
|
|
413
|
+
headers: resHeaders,
|
|
414
|
+
timestamp: Date.now(),
|
|
415
|
+
};
|
|
416
|
+
await saveHistory(historyEntry);
|
|
417
|
+
const updated = await loadHistory();
|
|
418
|
+
setHistory(updated);
|
|
419
|
+
} catch (err) {
|
|
420
|
+
setError(err instanceof Error ? err.message : "Request failed");
|
|
421
|
+
} finally {
|
|
422
|
+
setLoading(false);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const liveUrl = buildLiveUrl();
|
|
427
|
+
const reqHeaders = getRequestHeaders();
|
|
428
|
+
const hasBody = ["POST", "PUT", "PATCH"].includes(method);
|
|
429
|
+
const currentSnippet = generateSnippet(
|
|
430
|
+
snippetLang,
|
|
431
|
+
method,
|
|
432
|
+
liveUrl,
|
|
433
|
+
reqHeaders,
|
|
434
|
+
hasBody && bodyValue ? bodyValue : undefined,
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const displayResponse = historyView || response;
|
|
438
|
+
|
|
439
|
+
return (
|
|
440
|
+
<div className={cn("bd-playground", className)}>
|
|
441
|
+
{/* Header with method + live URL + send */}
|
|
442
|
+
<div className="bd-playground-header">
|
|
443
|
+
<span className={cn("bd-playground-method", METHOD_COLORS[method])}>
|
|
444
|
+
{method}
|
|
445
|
+
</span>
|
|
446
|
+
<code className="bd-playground-url">{liveUrl}</code>
|
|
447
|
+
<button className="bd-playground-send" onClick={sendRequest} disabled={loading}>
|
|
448
|
+
{loading ? (
|
|
449
|
+
<>
|
|
450
|
+
<svg className="bd-pg-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
451
|
+
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
|
|
452
|
+
</svg>
|
|
453
|
+
Sending...
|
|
454
|
+
</>
|
|
455
|
+
) : (
|
|
456
|
+
<>
|
|
457
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
458
|
+
<path d="M5 12h14M12 5l7 7-7 7" />
|
|
459
|
+
</svg>
|
|
460
|
+
Send
|
|
461
|
+
</>
|
|
462
|
+
)}
|
|
463
|
+
</button>
|
|
464
|
+
</div>
|
|
465
|
+
|
|
466
|
+
{/* Auth toggle */}
|
|
467
|
+
<div className="bd-pg-auth-bar">
|
|
468
|
+
<button className="bd-pg-auth-toggle" onClick={() => setShowAuth(!showAuth)}>
|
|
469
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
470
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
|
|
471
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
472
|
+
</svg>
|
|
473
|
+
Authentication
|
|
474
|
+
<svg
|
|
475
|
+
width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"
|
|
476
|
+
style={{ transform: showAuth ? "rotate(180deg)" : "none", transition: "transform 200ms" }}
|
|
477
|
+
>
|
|
478
|
+
<polyline points="6 9 12 15 18 9" />
|
|
479
|
+
</svg>
|
|
480
|
+
</button>
|
|
481
|
+
</div>
|
|
482
|
+
|
|
483
|
+
{showAuth && (
|
|
484
|
+
<div className="bd-pg-auth-panel">
|
|
485
|
+
<div className="bd-pg-auth-type">
|
|
486
|
+
<button
|
|
487
|
+
className={cn("bd-pg-auth-type-btn", authType === "bearer" && "active")}
|
|
488
|
+
onClick={() => setAuthType("bearer")}
|
|
489
|
+
>
|
|
490
|
+
Bearer Token
|
|
491
|
+
</button>
|
|
492
|
+
<button
|
|
493
|
+
className={cn("bd-pg-auth-type-btn", authType === "apikey" && "active")}
|
|
494
|
+
onClick={() => setAuthType("apikey")}
|
|
495
|
+
>
|
|
496
|
+
API Key
|
|
497
|
+
</button>
|
|
498
|
+
<button
|
|
499
|
+
className={cn("bd-pg-auth-type-btn", authType === "basic" && "active")}
|
|
500
|
+
onClick={() => setAuthType("basic")}
|
|
501
|
+
>
|
|
502
|
+
Basic Auth
|
|
503
|
+
</button>
|
|
504
|
+
</div>
|
|
505
|
+
{authType === "bearer" && (
|
|
506
|
+
<div className="bd-playground-param">
|
|
507
|
+
<label className="bd-playground-label">Token</label>
|
|
508
|
+
<input
|
|
509
|
+
type="text"
|
|
510
|
+
className="bd-playground-input"
|
|
511
|
+
value={bearerToken}
|
|
512
|
+
placeholder="Enter Bearer token..."
|
|
513
|
+
onChange={(e) => setBearerToken(e.target.value)}
|
|
514
|
+
/>
|
|
515
|
+
</div>
|
|
516
|
+
)}
|
|
517
|
+
{authType === "apikey" && (
|
|
518
|
+
<>
|
|
519
|
+
<div className="bd-playground-param">
|
|
520
|
+
<label className="bd-playground-label">Header Name</label>
|
|
521
|
+
<input
|
|
522
|
+
type="text"
|
|
523
|
+
className="bd-playground-input"
|
|
524
|
+
value={apiKeyName}
|
|
525
|
+
placeholder="X-API-Key"
|
|
526
|
+
onChange={(e) => setApiKeyName(e.target.value)}
|
|
527
|
+
/>
|
|
528
|
+
</div>
|
|
529
|
+
<div className="bd-playground-param">
|
|
530
|
+
<label className="bd-playground-label">Key Value</label>
|
|
531
|
+
<input
|
|
532
|
+
type="text"
|
|
533
|
+
className="bd-playground-input"
|
|
534
|
+
value={apiKeyValue}
|
|
535
|
+
placeholder="Enter API key..."
|
|
536
|
+
onChange={(e) => setApiKeyValue(e.target.value)}
|
|
537
|
+
/>
|
|
538
|
+
</div>
|
|
539
|
+
</>
|
|
540
|
+
)}
|
|
541
|
+
{authType === "basic" && (
|
|
542
|
+
<>
|
|
543
|
+
<div className="bd-playground-param">
|
|
544
|
+
<label className="bd-playground-label">Username</label>
|
|
545
|
+
<input
|
|
546
|
+
type="text"
|
|
547
|
+
className="bd-playground-input"
|
|
548
|
+
value={basicUser}
|
|
549
|
+
placeholder="Username"
|
|
550
|
+
onChange={(e) => setBasicUser(e.target.value)}
|
|
551
|
+
/>
|
|
552
|
+
</div>
|
|
553
|
+
<div className="bd-playground-param">
|
|
554
|
+
<label className="bd-playground-label">Password</label>
|
|
555
|
+
<input
|
|
556
|
+
type="password"
|
|
557
|
+
className="bd-playground-input"
|
|
558
|
+
value={basicPass}
|
|
559
|
+
placeholder="Password"
|
|
560
|
+
onChange={(e) => setBasicPass(e.target.value)}
|
|
561
|
+
/>
|
|
562
|
+
</div>
|
|
563
|
+
</>
|
|
564
|
+
)}
|
|
565
|
+
</div>
|
|
566
|
+
)}
|
|
567
|
+
|
|
568
|
+
{/* Parameters */}
|
|
569
|
+
{params.length > 0 && (
|
|
570
|
+
<div className="bd-playground-params">
|
|
571
|
+
{params.map((p) => (
|
|
572
|
+
<div key={p.name} className="bd-playground-param">
|
|
573
|
+
<label className="bd-playground-label">
|
|
574
|
+
<span className="bd-pg-param-name">{p.name}</span>
|
|
575
|
+
<span className="bd-playground-param-meta">
|
|
576
|
+
{p.in}
|
|
577
|
+
{p.required && <span className="bd-playground-required">*</span>}
|
|
578
|
+
</span>
|
|
579
|
+
</label>
|
|
580
|
+
{p.enum && p.enum.length > 0 ? (
|
|
581
|
+
<select
|
|
582
|
+
className="bd-playground-input bd-playground-select"
|
|
583
|
+
value={paramValues[p.name] || ""}
|
|
584
|
+
onChange={(e) =>
|
|
585
|
+
setParamValues((prev) => ({ ...prev, [p.name]: e.target.value }))
|
|
586
|
+
}
|
|
587
|
+
>
|
|
588
|
+
<option value="">Select {p.name}...</option>
|
|
589
|
+
{p.enum.map((val) => (
|
|
590
|
+
<option key={val} value={val}>{val}</option>
|
|
591
|
+
))}
|
|
592
|
+
</select>
|
|
593
|
+
) : (
|
|
594
|
+
<input
|
|
595
|
+
type="text"
|
|
596
|
+
className="bd-playground-input"
|
|
597
|
+
value={paramValues[p.name] || ""}
|
|
598
|
+
placeholder={p.description || p.type || ""}
|
|
599
|
+
onChange={(e) =>
|
|
600
|
+
setParamValues((prev) => ({ ...prev, [p.name]: e.target.value }))
|
|
601
|
+
}
|
|
602
|
+
/>
|
|
603
|
+
)}
|
|
604
|
+
</div>
|
|
605
|
+
))}
|
|
606
|
+
</div>
|
|
607
|
+
)}
|
|
608
|
+
|
|
609
|
+
{/* Body */}
|
|
610
|
+
{hasBody && (
|
|
611
|
+
<div className="bd-playground-body-section">
|
|
612
|
+
<label className="bd-playground-label">Request Body</label>
|
|
613
|
+
<textarea
|
|
614
|
+
className={cn("bd-playground-textarea", bodyError && "bd-playground-textarea-error")}
|
|
615
|
+
value={bodyValue}
|
|
616
|
+
onChange={(e) => {
|
|
617
|
+
setBodyValue(e.target.value);
|
|
618
|
+
if (bodyError) setBodyError(null);
|
|
619
|
+
}}
|
|
620
|
+
rows={6}
|
|
621
|
+
placeholder='{ "key": "value" }'
|
|
622
|
+
/>
|
|
623
|
+
{bodyError && (
|
|
624
|
+
<div className="bd-pg-body-error">
|
|
625
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
626
|
+
<circle cx="12" cy="12" r="10" />
|
|
627
|
+
<line x1="15" y1="9" x2="9" y2="15" />
|
|
628
|
+
<line x1="9" y1="9" x2="15" y2="15" />
|
|
629
|
+
</svg>
|
|
630
|
+
Invalid JSON: {bodyError}
|
|
631
|
+
</div>
|
|
632
|
+
)}
|
|
633
|
+
</div>
|
|
634
|
+
)}
|
|
635
|
+
|
|
636
|
+
{/* Code Snippets */}
|
|
637
|
+
<div className="bd-pg-code-section">
|
|
638
|
+
<div className="bd-pg-code-tabs">
|
|
639
|
+
{(Object.keys(SNIPPET_LABELS) as SnippetLang[]).map((lang) => (
|
|
640
|
+
<button
|
|
641
|
+
key={lang}
|
|
642
|
+
className={cn("bd-pg-code-tab", snippetLang === lang && "active")}
|
|
643
|
+
onClick={() => setSnippetLang(lang)}
|
|
644
|
+
>
|
|
645
|
+
{SNIPPET_LABELS[lang]}
|
|
646
|
+
</button>
|
|
647
|
+
))}
|
|
648
|
+
<CopyButton text={currentSnippet} />
|
|
649
|
+
</div>
|
|
650
|
+
<pre className="bd-pg-code-pre">
|
|
651
|
+
<code>{currentSnippet}</code>
|
|
652
|
+
</pre>
|
|
653
|
+
</div>
|
|
654
|
+
|
|
655
|
+
{/* Response */}
|
|
656
|
+
{(displayResponse || error) && (
|
|
657
|
+
<div className="bd-playground-response">
|
|
658
|
+
{error ? (
|
|
659
|
+
<div className="bd-playground-error">{error}</div>
|
|
660
|
+
) : displayResponse ? (
|
|
661
|
+
<>
|
|
662
|
+
<div className="bd-playground-response-header">
|
|
663
|
+
<div className="bd-pg-response-meta">
|
|
664
|
+
<span
|
|
665
|
+
className={cn(
|
|
666
|
+
"bd-playground-status",
|
|
667
|
+
displayResponse.status < 300 ? "bd-status-ok" : displayResponse.status < 500 ? "bd-status-warn" : "bd-status-err",
|
|
668
|
+
)}
|
|
669
|
+
>
|
|
670
|
+
{displayResponse.status}
|
|
671
|
+
</span>
|
|
672
|
+
<span className="bd-pg-status-text">{displayResponse.statusText}</span>
|
|
673
|
+
<span className="bd-playground-time">{displayResponse.time}ms</span>
|
|
674
|
+
<span className="bd-playground-size">{formatSize(displayResponse.size)}</span>
|
|
675
|
+
</div>
|
|
676
|
+
<div className="bd-pg-response-tabs">
|
|
677
|
+
<button
|
|
678
|
+
className={cn("bd-pg-resp-tab", responseTab === "body" && "active")}
|
|
679
|
+
onClick={() => setResponseTab("body")}
|
|
680
|
+
>
|
|
681
|
+
Body
|
|
682
|
+
</button>
|
|
683
|
+
<button
|
|
684
|
+
className={cn("bd-pg-resp-tab", responseTab === "headers" && "active")}
|
|
685
|
+
onClick={() => setResponseTab("headers")}
|
|
686
|
+
>
|
|
687
|
+
Headers
|
|
688
|
+
<span className="bd-pg-badge">{displayResponse.headers.length}</span>
|
|
689
|
+
</button>
|
|
690
|
+
</div>
|
|
691
|
+
</div>
|
|
692
|
+
|
|
693
|
+
{historyView && (
|
|
694
|
+
<div className="bd-pg-history-banner">
|
|
695
|
+
Viewing history entry
|
|
696
|
+
<button className="bd-pg-history-dismiss" onClick={() => setHistoryView(null)}>
|
|
697
|
+
Back to current
|
|
698
|
+
</button>
|
|
699
|
+
</div>
|
|
700
|
+
)}
|
|
701
|
+
|
|
702
|
+
{responseTab === "body" && (
|
|
703
|
+
<div className="bd-pg-response-body">
|
|
704
|
+
<CopyButton text={displayResponse.body} />
|
|
705
|
+
<pre className="bd-playground-pre">
|
|
706
|
+
<code>{highlightJson(displayResponse.body)}</code>
|
|
707
|
+
</pre>
|
|
708
|
+
</div>
|
|
709
|
+
)}
|
|
710
|
+
|
|
711
|
+
{responseTab === "headers" && (
|
|
712
|
+
<div className="bd-pg-headers-table">
|
|
713
|
+
<table>
|
|
714
|
+
<thead>
|
|
715
|
+
<tr>
|
|
716
|
+
<th>Header</th>
|
|
717
|
+
<th>Value</th>
|
|
718
|
+
</tr>
|
|
719
|
+
</thead>
|
|
720
|
+
<tbody>
|
|
721
|
+
{displayResponse.headers.map(([k, v]) => (
|
|
722
|
+
<tr key={k}>
|
|
723
|
+
<td><code>{k}</code></td>
|
|
724
|
+
<td>{v}</td>
|
|
725
|
+
</tr>
|
|
726
|
+
))}
|
|
727
|
+
</tbody>
|
|
728
|
+
</table>
|
|
729
|
+
</div>
|
|
730
|
+
)}
|
|
731
|
+
</>
|
|
732
|
+
) : null}
|
|
733
|
+
</div>
|
|
734
|
+
)}
|
|
735
|
+
|
|
736
|
+
{/* History */}
|
|
737
|
+
{history.length > 0 && (
|
|
738
|
+
<div className="bd-pg-history-section">
|
|
739
|
+
<button
|
|
740
|
+
className="bd-pg-history-toggle"
|
|
741
|
+
onClick={() => setShowHistory(!showHistory)}
|
|
742
|
+
>
|
|
743
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
744
|
+
<circle cx="12" cy="12" r="10" />
|
|
745
|
+
<polyline points="12 6 12 12 16 14" />
|
|
746
|
+
</svg>
|
|
747
|
+
History
|
|
748
|
+
<span className="bd-pg-badge">{history.length}</span>
|
|
749
|
+
<svg
|
|
750
|
+
width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"
|
|
751
|
+
style={{ transform: showHistory ? "rotate(180deg)" : "none", transition: "transform 200ms", marginLeft: "auto" }}
|
|
752
|
+
>
|
|
753
|
+
<polyline points="6 9 12 15 18 9" />
|
|
754
|
+
</svg>
|
|
755
|
+
</button>
|
|
756
|
+
{showHistory && (
|
|
757
|
+
<div className="bd-pg-history-list">
|
|
758
|
+
{history.map((entry) => (
|
|
759
|
+
<button
|
|
760
|
+
key={entry.id}
|
|
761
|
+
className={cn("bd-pg-history-item", historyView?.id === entry.id && "active")}
|
|
762
|
+
onClick={() => {
|
|
763
|
+
setHistoryView(entry);
|
|
764
|
+
setResponseTab("body");
|
|
765
|
+
setError(null);
|
|
766
|
+
}}
|
|
767
|
+
>
|
|
768
|
+
<span className={cn("bd-pg-history-method", METHOD_COLORS[entry.method])}>
|
|
769
|
+
{entry.method}
|
|
770
|
+
</span>
|
|
771
|
+
<span className="bd-pg-history-url">{entry.url.replace(/^https?:\/\/[^/]+/, "")}</span>
|
|
772
|
+
<span
|
|
773
|
+
className={cn(
|
|
774
|
+
"bd-pg-history-status",
|
|
775
|
+
entry.status < 300 ? "bd-status-ok" : entry.status < 500 ? "bd-status-warn" : "bd-status-err",
|
|
776
|
+
)}
|
|
777
|
+
>
|
|
778
|
+
{entry.status}
|
|
779
|
+
</span>
|
|
780
|
+
<span className="bd-pg-history-time">{entry.time}ms</span>
|
|
781
|
+
</button>
|
|
782
|
+
))}
|
|
783
|
+
</div>
|
|
784
|
+
)}
|
|
785
|
+
</div>
|
|
786
|
+
)}
|
|
787
|
+
</div>
|
|
788
|
+
);
|
|
789
|
+
}
|