@barodoc/theme-docs 6.0.0 → 7.0.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 +2 -2
- package/src/components/Contributors.astro +32 -16
- package/src/components/DocHeader.tsx +29 -1
- package/src/components/Header.astro +1 -0
- package/src/components/MobileNav.astro +2 -0
- package/src/components/MobileNavSheet.tsx +29 -1
- package/src/components/index.ts +1 -0
- package/src/components/mdx/ApiEndpoint.tsx +35 -0
- package/src/components/mdx/ApiPlayground.tsx +650 -61
- package/src/layouts/BlogLayout.astro +16 -2
- package/src/pages/blog/[...slug].astro +1 -0
- package/src/pages/blog/index.astro +12 -1
- package/src/pages/docs/[...slug].astro +1 -1
- package/src/pages/index.astro +18 -4
- package/src/styles/global.css +998 -114
|
@@ -8,6 +8,7 @@ interface ParamDef {
|
|
|
8
8
|
required?: boolean;
|
|
9
9
|
defaultValue?: string;
|
|
10
10
|
description?: string;
|
|
11
|
+
enum?: string[];
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
interface ApiPlaygroundProps {
|
|
@@ -19,14 +20,223 @@ interface ApiPlaygroundProps {
|
|
|
19
20
|
className?: string;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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",
|
|
28
42
|
};
|
|
29
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
|
+
|
|
30
240
|
export function ApiPlayground({
|
|
31
241
|
method = "GET",
|
|
32
242
|
url,
|
|
@@ -37,25 +247,60 @@ export function ApiPlayground({
|
|
|
37
247
|
}: ApiPlaygroundProps) {
|
|
38
248
|
const [paramValues, setParamValues] = React.useState<Record<string, string>>(() => {
|
|
39
249
|
const vals: Record<string, string> = {};
|
|
40
|
-
for (const p of params)
|
|
41
|
-
vals[p.name] = p.defaultValue || "";
|
|
42
|
-
}
|
|
250
|
+
for (const p of params) vals[p.name] = p.defaultValue || "";
|
|
43
251
|
return vals;
|
|
44
252
|
});
|
|
45
253
|
const [bodyValue, setBodyValue] = React.useState(initialBody || "");
|
|
254
|
+
const [bodyError, setBodyError] = React.useState<string | null>(null);
|
|
46
255
|
const [response, setResponse] = React.useState<{
|
|
47
256
|
status: number;
|
|
48
257
|
statusText: string;
|
|
49
258
|
body: string;
|
|
259
|
+
headers: [string, string][];
|
|
50
260
|
time: number;
|
|
261
|
+
size: number;
|
|
51
262
|
} | null>(null);
|
|
52
263
|
const [loading, setLoading] = React.useState(false);
|
|
53
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
|
+
}, []);
|
|
54
300
|
|
|
55
301
|
function buildUrl(): string {
|
|
56
302
|
let finalUrl = url;
|
|
57
303
|
const queryParams = new URLSearchParams();
|
|
58
|
-
|
|
59
304
|
for (const p of params) {
|
|
60
305
|
const val = paramValues[p.name] || "";
|
|
61
306
|
if (p.in === "path") {
|
|
@@ -64,31 +309,75 @@ export function ApiPlayground({
|
|
|
64
309
|
queryParams.set(p.name, val);
|
|
65
310
|
}
|
|
66
311
|
}
|
|
67
|
-
|
|
68
312
|
const qs = queryParams.toString();
|
|
69
313
|
return qs ? `${finalUrl}?${qs}` : finalUrl;
|
|
70
314
|
}
|
|
71
315
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
}
|
|
79
334
|
|
|
80
|
-
|
|
335
|
+
function getRequestHeaders(): Record<string, string> {
|
|
336
|
+
const h: Record<string, string> = { ...initialHeaders };
|
|
81
337
|
for (const p of params) {
|
|
82
338
|
if (p.in === "header" && paramValues[p.name]) {
|
|
83
|
-
|
|
339
|
+
h[p.name] = paramValues[p.name];
|
|
84
340
|
}
|
|
85
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
|
+
}
|
|
86
357
|
|
|
358
|
+
async function sendRequest() {
|
|
87
359
|
const hasBody = ["POST", "PUT", "PATCH"].includes(method) && bodyValue;
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
}
|
|
90
369
|
}
|
|
91
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
|
+
|
|
92
381
|
try {
|
|
93
382
|
const res = await fetch(reqUrl, {
|
|
94
383
|
method,
|
|
@@ -97,15 +386,36 @@ export function ApiPlayground({
|
|
|
97
386
|
});
|
|
98
387
|
|
|
99
388
|
const elapsed = Math.round(performance.now() - start);
|
|
389
|
+
const rawText = await res.text();
|
|
390
|
+
const size = new Blob([rawText]).size;
|
|
100
391
|
let text: string;
|
|
101
392
|
try {
|
|
102
|
-
const json =
|
|
393
|
+
const json = JSON.parse(rawText);
|
|
103
394
|
text = JSON.stringify(json, null, 2);
|
|
104
395
|
} catch {
|
|
105
|
-
text =
|
|
396
|
+
text = rawText;
|
|
106
397
|
}
|
|
107
398
|
|
|
108
|
-
|
|
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);
|
|
109
419
|
} catch (err) {
|
|
110
420
|
setError(err instanceof Error ? err.message : "Request failed");
|
|
111
421
|
} finally {
|
|
@@ -113,88 +423,367 @@ export function ApiPlayground({
|
|
|
113
423
|
}
|
|
114
424
|
}
|
|
115
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
|
+
|
|
116
439
|
return (
|
|
117
440
|
<div className={cn("bd-playground", className)}>
|
|
118
|
-
{/* Header */}
|
|
441
|
+
{/* Header with method + live URL + send */}
|
|
119
442
|
<div className="bd-playground-header">
|
|
120
|
-
<span className={cn("bd-playground-method",
|
|
443
|
+
<span className={cn("bd-playground-method", METHOD_COLORS[method])}>
|
|
121
444
|
{method}
|
|
122
445
|
</span>
|
|
123
|
-
<code className="bd-playground-url">{
|
|
124
|
-
<button
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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>
|
|
130
480
|
</button>
|
|
131
481
|
</div>
|
|
132
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
|
+
|
|
133
568
|
{/* Parameters */}
|
|
134
569
|
{params.length > 0 && (
|
|
135
570
|
<div className="bd-playground-params">
|
|
136
571
|
{params.map((p) => (
|
|
137
572
|
<div key={p.name} className="bd-playground-param">
|
|
138
573
|
<label className="bd-playground-label">
|
|
139
|
-
<span>{p.name}</span>
|
|
574
|
+
<span className="bd-pg-param-name">{p.name}</span>
|
|
140
575
|
<span className="bd-playground-param-meta">
|
|
141
576
|
{p.in}
|
|
142
577
|
{p.required && <span className="bd-playground-required">*</span>}
|
|
143
578
|
</span>
|
|
144
579
|
</label>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
)}
|
|
154
604
|
</div>
|
|
155
605
|
))}
|
|
156
606
|
</div>
|
|
157
607
|
)}
|
|
158
608
|
|
|
159
609
|
{/* Body */}
|
|
160
|
-
{
|
|
610
|
+
{hasBody && (
|
|
161
611
|
<div className="bd-playground-body-section">
|
|
162
612
|
<label className="bd-playground-label">Request Body</label>
|
|
163
613
|
<textarea
|
|
164
|
-
className="bd-playground-textarea"
|
|
614
|
+
className={cn("bd-playground-textarea", bodyError && "bd-playground-textarea-error")}
|
|
165
615
|
value={bodyValue}
|
|
166
|
-
onChange={(e) =>
|
|
616
|
+
onChange={(e) => {
|
|
617
|
+
setBodyValue(e.target.value);
|
|
618
|
+
if (bodyError) setBodyError(null);
|
|
619
|
+
}}
|
|
167
620
|
rows={6}
|
|
168
621
|
placeholder='{ "key": "value" }'
|
|
169
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
|
+
)}
|
|
170
633
|
</div>
|
|
171
634
|
)}
|
|
172
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
|
+
|
|
173
655
|
{/* Response */}
|
|
174
|
-
{(
|
|
656
|
+
{(displayResponse || error) && (
|
|
175
657
|
<div className="bd-playground-response">
|
|
176
658
|
{error ? (
|
|
177
659
|
<div className="bd-playground-error">{error}</div>
|
|
178
|
-
) :
|
|
660
|
+
) : displayResponse ? (
|
|
179
661
|
<>
|
|
180
662
|
<div className="bd-playground-response-header">
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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>
|
|
190
691
|
</div>
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
+
)}
|
|
194
731
|
</>
|
|
195
732
|
) : null}
|
|
196
733
|
</div>
|
|
197
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
|
+
)}
|
|
198
787
|
</div>
|
|
199
788
|
);
|
|
200
789
|
}
|