@hustle-together/api-dev-tools 3.12.2 → 3.12.10
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/.claude/api-dev-state.json +224 -165
- package/CHANGELOG.md +29 -0
- package/README.md +58 -1
- package/bin/cli.js +1303 -89
- package/hooks/update-api-showcase.py +13 -1
- package/hooks/update-ui-showcase.py +13 -1
- package/package.json +7 -3
- package/scripts/extract-schema-docs.cjs +322 -0
- package/templates/api-showcase/_components/APIModal.tsx +100 -8
- package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
- package/templates/api-showcase/_components/APITester.tsx +132 -45
- package/templates/typedoc.json +19 -0
- package/templates/ui-showcase/_components/UIShowcase.tsx +1 -1
- package/templates/ui-showcase/page.tsx +1 -1
|
@@ -9,6 +9,9 @@ interface ParameterDoc {
|
|
|
9
9
|
required?: boolean;
|
|
10
10
|
default?: string | number | boolean;
|
|
11
11
|
enum?: string[];
|
|
12
|
+
example?: string;
|
|
13
|
+
min?: number;
|
|
14
|
+
max?: number;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
interface SchemaDoc {
|
|
@@ -17,6 +20,13 @@ interface SchemaDoc {
|
|
|
17
20
|
queryParams?: ParameterDoc[];
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
/** Example request from registry.json */
|
|
24
|
+
interface EndpointExample {
|
|
25
|
+
description: string;
|
|
26
|
+
query: string;
|
|
27
|
+
curl: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
interface APITesterProps {
|
|
21
31
|
id: string;
|
|
22
32
|
endpoint: string;
|
|
@@ -24,6 +34,16 @@ interface APITesterProps {
|
|
|
24
34
|
selectedEndpoint?: string | null;
|
|
25
35
|
schemaPath?: string;
|
|
26
36
|
schema?: SchemaDoc;
|
|
37
|
+
/** Parameters from registry.json endpoints section */
|
|
38
|
+
endpointParams?: ParameterDoc[];
|
|
39
|
+
/** API route file path for reference */
|
|
40
|
+
apiRoute?: string;
|
|
41
|
+
/** Pre-built examples from registry.json */
|
|
42
|
+
examples?: Record<string, EndpointExample>;
|
|
43
|
+
/** Callback to expose submit function to parent */
|
|
44
|
+
onSubmitRef?: (submitFn: () => Promise<void>) => void;
|
|
45
|
+
/** Callback to notify parent of loading state */
|
|
46
|
+
onLoadingChange?: (isLoading: boolean) => void;
|
|
27
47
|
}
|
|
28
48
|
|
|
29
49
|
interface RequestState {
|
|
@@ -80,7 +100,7 @@ const DEFAULT_QUERY_PARAMS: Record<string, Record<string, string>> = {
|
|
|
80
100
|
* - Response display with timing
|
|
81
101
|
* - Audio playback for binary responses
|
|
82
102
|
*
|
|
83
|
-
* Created with Hustle API Dev Tools (v3.
|
|
103
|
+
* Created with Hustle API Dev Tools (v3.12.10)
|
|
84
104
|
*/
|
|
85
105
|
export function APITester({
|
|
86
106
|
id,
|
|
@@ -89,7 +109,13 @@ export function APITester({
|
|
|
89
109
|
selectedEndpoint,
|
|
90
110
|
schemaPath,
|
|
91
111
|
schema,
|
|
112
|
+
endpointParams,
|
|
113
|
+
apiRoute,
|
|
114
|
+
examples,
|
|
115
|
+
onSubmitRef,
|
|
116
|
+
onLoadingChange,
|
|
92
117
|
}: APITesterProps) {
|
|
118
|
+
const [selectedExample, setSelectedExample] = useState<string | null>(null);
|
|
93
119
|
// Get default body for this API/endpoint
|
|
94
120
|
const getDefaultBody = () => {
|
|
95
121
|
const apiDefaults = DEFAULT_BODIES[id];
|
|
@@ -104,10 +130,30 @@ export function APITester({
|
|
|
104
130
|
|
|
105
131
|
// Get default query params for GET requests
|
|
106
132
|
const getDefaultQueryParams = () => {
|
|
133
|
+
// First check hardcoded defaults
|
|
107
134
|
const apiParams = DEFAULT_QUERY_PARAMS[id];
|
|
108
135
|
if (apiParams && selectedEndpoint) {
|
|
109
136
|
return apiParams[selectedEndpoint] || "";
|
|
110
137
|
}
|
|
138
|
+
|
|
139
|
+
// Build from endpointParams if available
|
|
140
|
+
if (endpointParams && endpointParams.length > 0) {
|
|
141
|
+
const queryParts: string[] = [];
|
|
142
|
+
for (const param of endpointParams) {
|
|
143
|
+
if (param.name === "action") {
|
|
144
|
+
// Add action with the selectedEndpoint or default value
|
|
145
|
+
queryParts.push(`action=${selectedEndpoint || param.default || ""}`);
|
|
146
|
+
} else if (param.required && param.example) {
|
|
147
|
+
// Add required params with example values
|
|
148
|
+
queryParts.push(`${param.name}=${encodeURIComponent(param.example)}`);
|
|
149
|
+
} else if (param.required && param.default !== undefined) {
|
|
150
|
+
// Add required params with defaults
|
|
151
|
+
queryParts.push(`${param.name}=${encodeURIComponent(String(param.default))}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return queryParts.join("&");
|
|
155
|
+
}
|
|
156
|
+
|
|
111
157
|
return "";
|
|
112
158
|
};
|
|
113
159
|
|
|
@@ -132,6 +178,21 @@ export function APITester({
|
|
|
132
178
|
const [isLoading, setIsLoading] = useState(false);
|
|
133
179
|
const [audioUrl, setAudioUrl] = useState<string | null>(null);
|
|
134
180
|
|
|
181
|
+
// Expose submit function to parent for footer button
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (onSubmitRef) {
|
|
184
|
+
onSubmitRef(handleSubmit);
|
|
185
|
+
}
|
|
186
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
187
|
+
}, [onSubmitRef, request, endpoint]);
|
|
188
|
+
|
|
189
|
+
// Notify parent of loading state changes
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (onLoadingChange) {
|
|
192
|
+
onLoadingChange(isLoading);
|
|
193
|
+
}
|
|
194
|
+
}, [isLoading, onLoadingChange]);
|
|
195
|
+
|
|
135
196
|
// Update defaults when endpoint changes
|
|
136
197
|
useEffect(() => {
|
|
137
198
|
setRequest((prev) => ({
|
|
@@ -140,7 +201,7 @@ export function APITester({
|
|
|
140
201
|
body: getDefaultBody(),
|
|
141
202
|
queryParams: getDefaultQueryParams(),
|
|
142
203
|
}));
|
|
143
|
-
// Clear previous response
|
|
204
|
+
// Clear previous response and example selection
|
|
144
205
|
setResponse({
|
|
145
206
|
status: null,
|
|
146
207
|
statusText: "",
|
|
@@ -150,8 +211,9 @@ export function APITester({
|
|
|
150
211
|
contentType: null,
|
|
151
212
|
});
|
|
152
213
|
setAudioUrl(null);
|
|
214
|
+
setSelectedExample(null);
|
|
153
215
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
154
|
-
}, [selectedEndpoint, id]);
|
|
216
|
+
}, [selectedEndpoint, id, endpointParams, examples]);
|
|
155
217
|
|
|
156
218
|
const handleSubmit = async () => {
|
|
157
219
|
setIsLoading(true);
|
|
@@ -298,6 +360,49 @@ export function APITester({
|
|
|
298
360
|
</div>
|
|
299
361
|
</div>
|
|
300
362
|
|
|
363
|
+
{/* Example Selector (for GET requests with examples) */}
|
|
364
|
+
{request.method === "GET" && examples && Object.keys(examples).length > 0 && (
|
|
365
|
+
<div>
|
|
366
|
+
<label className="mb-1 block text-sm font-bold text-black dark:text-white">
|
|
367
|
+
Example Requests
|
|
368
|
+
</label>
|
|
369
|
+
<div className="flex flex-wrap gap-2">
|
|
370
|
+
{Object.entries(examples).map(([key, example]) => (
|
|
371
|
+
<button
|
|
372
|
+
key={key}
|
|
373
|
+
onClick={() => {
|
|
374
|
+
setSelectedExample(key);
|
|
375
|
+
setRequest((prev) => ({ ...prev, queryParams: example.query }));
|
|
376
|
+
}}
|
|
377
|
+
className={`border-2 px-3 py-1.5 text-xs font-medium transition-colors ${
|
|
378
|
+
selectedExample === key
|
|
379
|
+
? "border-[#BA0C2F] bg-[#BA0C2F] text-white"
|
|
380
|
+
: "border-black bg-white text-black hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
381
|
+
}`}
|
|
382
|
+
title={example.description}
|
|
383
|
+
>
|
|
384
|
+
{key.replace(/_/g, " ")}
|
|
385
|
+
</button>
|
|
386
|
+
))}
|
|
387
|
+
</div>
|
|
388
|
+
{selectedExample && examples[selectedExample] && (
|
|
389
|
+
<div className="mt-2 flex items-center gap-2">
|
|
390
|
+
<p className="text-xs text-gray-600 dark:text-gray-400">
|
|
391
|
+
{examples[selectedExample].description}
|
|
392
|
+
</p>
|
|
393
|
+
<button
|
|
394
|
+
onClick={() => {
|
|
395
|
+
navigator.clipboard.writeText(examples[selectedExample].curl);
|
|
396
|
+
}}
|
|
397
|
+
className="shrink-0 border border-gray-300 bg-gray-100 px-2 py-0.5 text-xs text-gray-600 hover:bg-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300"
|
|
398
|
+
>
|
|
399
|
+
Copy curl
|
|
400
|
+
</button>
|
|
401
|
+
</div>
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
)}
|
|
405
|
+
|
|
301
406
|
{/* Query Parameters (for GET requests) */}
|
|
302
407
|
{request.method === "GET" && (
|
|
303
408
|
<div>
|
|
@@ -307,14 +412,15 @@ export function APITester({
|
|
|
307
412
|
<input
|
|
308
413
|
type="text"
|
|
309
414
|
value={request.queryParams}
|
|
310
|
-
onChange={(e) =>
|
|
311
|
-
|
|
312
|
-
|
|
415
|
+
onChange={(e) => {
|
|
416
|
+
setSelectedExample(null); // Clear example selection when manually editing
|
|
417
|
+
setRequest((prev) => ({ ...prev, queryParams: e.target.value }));
|
|
418
|
+
}}
|
|
313
419
|
className="w-full border-2 border-black bg-white px-3 py-2 font-mono text-sm focus:border-[#BA0C2F] focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
314
420
|
placeholder="key1=value1&key2=value2"
|
|
315
421
|
/>
|
|
316
422
|
<p className="mt-1 text-xs text-gray-600 dark:text-gray-400">
|
|
317
|
-
Add query string parameters (without the ?)
|
|
423
|
+
Add query string parameters (without the ?) - or select an example above
|
|
318
424
|
</p>
|
|
319
425
|
</div>
|
|
320
426
|
)}
|
|
@@ -347,10 +453,11 @@ export function APITester({
|
|
|
347
453
|
)}
|
|
348
454
|
|
|
349
455
|
{/* Parameter Documentation */}
|
|
350
|
-
{schema && (schema.request?.length || schema.queryParams?.length)
|
|
456
|
+
{(schema && (schema.request?.length || schema.queryParams?.length)) ||
|
|
457
|
+
endpointParams?.length ? (
|
|
351
458
|
<ParameterDocs
|
|
352
|
-
requestParams={schema
|
|
353
|
-
queryParams={schema
|
|
459
|
+
requestParams={schema?.request}
|
|
460
|
+
queryParams={schema?.queryParams || endpointParams}
|
|
354
461
|
isGetRequest={request.method === "GET"}
|
|
355
462
|
/>
|
|
356
463
|
) : null}
|
|
@@ -376,41 +483,6 @@ export function APITester({
|
|
|
376
483
|
API keys loaded from .env automatically
|
|
377
484
|
</p>
|
|
378
485
|
</div>
|
|
379
|
-
|
|
380
|
-
{/* Submit Button */}
|
|
381
|
-
<button
|
|
382
|
-
onClick={handleSubmit}
|
|
383
|
-
disabled={isLoading}
|
|
384
|
-
className="w-full border-2 border-black bg-[#BA0C2F] py-3 font-bold text-white transition-colors hover:bg-[#8a0923] disabled:opacity-50"
|
|
385
|
-
>
|
|
386
|
-
{isLoading ? (
|
|
387
|
-
<span className="flex items-center justify-center gap-2">
|
|
388
|
-
<svg
|
|
389
|
-
className="h-4 w-4 animate-spin"
|
|
390
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
391
|
-
fill="none"
|
|
392
|
-
viewBox="0 0 24 24"
|
|
393
|
-
>
|
|
394
|
-
<circle
|
|
395
|
-
className="opacity-25"
|
|
396
|
-
cx="12"
|
|
397
|
-
cy="12"
|
|
398
|
-
r="10"
|
|
399
|
-
stroke="currentColor"
|
|
400
|
-
strokeWidth="4"
|
|
401
|
-
/>
|
|
402
|
-
<path
|
|
403
|
-
className="opacity-75"
|
|
404
|
-
fill="currentColor"
|
|
405
|
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
406
|
-
/>
|
|
407
|
-
</svg>
|
|
408
|
-
Sending...
|
|
409
|
-
</span>
|
|
410
|
-
) : (
|
|
411
|
-
`Send ${request.method} Request`
|
|
412
|
-
)}
|
|
413
|
-
</button>
|
|
414
486
|
</div>
|
|
415
487
|
|
|
416
488
|
{/* Response Panel */}
|
|
@@ -574,6 +646,21 @@ function ParameterDocs({
|
|
|
574
646
|
</code>
|
|
575
647
|
</p>
|
|
576
648
|
)}
|
|
649
|
+
{param.example && (
|
|
650
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
651
|
+
Example:{" "}
|
|
652
|
+
<code className="text-gray-700 dark:text-gray-300">
|
|
653
|
+
{param.example}
|
|
654
|
+
</code>
|
|
655
|
+
</p>
|
|
656
|
+
)}
|
|
657
|
+
{(param.min !== undefined || param.max !== undefined) && (
|
|
658
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
659
|
+
{param.min !== undefined && `Min: ${param.min}`}
|
|
660
|
+
{param.min !== undefined && param.max !== undefined && " · "}
|
|
661
|
+
{param.max !== undefined && `Max: ${param.max}`}
|
|
662
|
+
</p>
|
|
663
|
+
)}
|
|
577
664
|
</div>
|
|
578
665
|
))}
|
|
579
666
|
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://typedoc.org/schema.json",
|
|
3
|
+
"entryPoints": ["src/lib/schemas/*.ts", "src/app/api/**/*.ts"],
|
|
4
|
+
"out": "docs/api",
|
|
5
|
+
"plugin": ["typedoc-plugin-markdown"],
|
|
6
|
+
"exclude": ["**/*.test.ts", "**/__tests__/**", "**/node_modules/**"],
|
|
7
|
+
"excludePrivate": true,
|
|
8
|
+
"excludeInternal": true,
|
|
9
|
+
"readme": "none",
|
|
10
|
+
"name": "API Documentation",
|
|
11
|
+
"includeVersion": true,
|
|
12
|
+
"categorizeByGroup": true,
|
|
13
|
+
"sort": ["alphabetical"],
|
|
14
|
+
"validation": {
|
|
15
|
+
"notExported": false,
|
|
16
|
+
"invalidLink": false,
|
|
17
|
+
"notDocumented": false
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState, useMemo } from "react";
|
|
4
|
-
import { HeroHeader } from "
|
|
4
|
+
import { HeroHeader } from "../../shared/HeroHeader";
|
|
5
5
|
import { PreviewCard } from "./PreviewCard";
|
|
6
6
|
import { PreviewModal } from "./PreviewModal";
|
|
7
7
|
|