@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.
@@ -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.9.2)
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
- setRequest((prev) => ({ ...prev, queryParams: e.target.value }))
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.request}
353
- queryParams={schema.queryParams}
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 "../shared/HeroHeader";
4
+ import { HeroHeader } from "../../shared/HeroHeader";
5
5
  import { PreviewCard } from "./PreviewCard";
6
6
  import { PreviewModal } from "./PreviewModal";
7
7
 
@@ -1,5 +1,5 @@
1
1
  import type { Metadata } from "next";
2
- import { UIShowcase } from "./UIShowcase";
2
+ import { UIShowcase } from "./_components/UIShowcase";
3
3
 
4
4
  export const metadata: Metadata = {
5
5
  title: "UI Showcase",