@hustle-together/api-dev-tools 3.12.3 → 4.5.1
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/adr-requests/.gitkeep +10 -0
- package/.claude/agents/adr-researcher.md +109 -0
- package/.claude/agents/visual-analyzer.md +183 -0
- package/.claude/api-dev-state.json +7 -463
- package/.claude/documentation-audit.json +114 -0
- package/.claude/registry.json +289 -0
- package/.claude/settings.json +45 -1
- package/.claude/workflow-logs/None.json +49 -0
- package/.claude/workflow-logs/session-20251230-143727.json +106 -0
- package/.skills/adr-deep-research/SKILL.md +351 -0
- package/.skills/api-create/SKILL.md +116 -17
- package/.skills/api-research/SKILL.md +130 -0
- package/.skills/docs-sync/SKILL.md +260 -0
- package/.skills/docs-update/SKILL.md +205 -0
- package/.skills/hustle-brand/SKILL.md +368 -0
- package/.skills/hustle-build/SKILL.md +786 -0
- package/.skills/hustle-build-review/SKILL.md +518 -0
- package/.skills/parallel-spawn/SKILL.md +212 -0
- package/.skills/ralph-continue/SKILL.md +151 -0
- package/.skills/ralph-loop/SKILL.md +341 -0
- package/.skills/ralph-status/SKILL.md +87 -0
- package/.skills/refactor/SKILL.md +59 -0
- package/.skills/shadcn/SKILL.md +522 -0
- package/.skills/test-all/SKILL.md +210 -0
- package/.skills/test-builds/SKILL.md +208 -0
- package/.skills/test-debug/SKILL.md +212 -0
- package/.skills/test-e2e/SKILL.md +168 -0
- package/.skills/test-review/SKILL.md +707 -0
- package/.skills/test-unit/SKILL.md +143 -0
- package/.skills/test-visual/SKILL.md +301 -0
- package/.skills/token-report/SKILL.md +132 -0
- package/CHANGELOG.md +575 -0
- package/README.md +426 -56
- package/bin/cli.js +1538 -88
- package/commands/hustle-api-create.md +22 -0
- package/commands/hustle-build.md +259 -0
- package/commands/hustle-combine.md +81 -2
- package/commands/hustle-ui-create-page.md +84 -2
- package/commands/hustle-ui-create.md +82 -2
- package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
- package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
- package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
- package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
- package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
- package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
- package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
- package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
- package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
- package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
- package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
- package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
- package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
- package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
- package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
- package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
- package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
- package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
- package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
- package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
- package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
- package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
- package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
- package/hooks/api-workflow-check.py +34 -0
- package/hooks/auto-answer.py +305 -0
- package/hooks/check-update.py +132 -0
- package/hooks/completion-promise-detector.py +293 -0
- package/hooks/context-capacity-warning.py +171 -0
- package/hooks/docs-update-check.py +120 -0
- package/hooks/enforce-dry-run.py +134 -0
- package/hooks/enforce-external-research.py +25 -0
- package/hooks/enforce-interview.py +20 -0
- package/hooks/generate-adr-options.py +282 -0
- package/hooks/hook_utils.py +609 -0
- package/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
- package/hooks/ntfy-on-question.py +240 -0
- package/hooks/orchestrator-completion.py +313 -0
- package/hooks/orchestrator-handoff.py +267 -0
- package/hooks/orchestrator-session-startup.py +146 -0
- package/hooks/parallel-orchestrator.py +451 -0
- package/hooks/periodic-reground.py +270 -67
- package/hooks/project-document-prompt.py +302 -0
- package/hooks/remote-question-proxy.py +284 -0
- package/hooks/remote-question-server.py +1224 -0
- package/hooks/run-code-review.py +176 -29
- package/hooks/run-visual-qa.py +338 -0
- package/hooks/session-logger.py +27 -1
- package/hooks/session-startup.py +113 -0
- package/hooks/update-adr-decision.py +236 -0
- package/hooks/update-api-showcase.py +13 -1
- package/hooks/update-testing-checklist.py +195 -0
- package/hooks/update-ui-showcase.py +13 -1
- package/package.json +7 -3
- package/scripts/extract-schema-docs.cjs +322 -0
- package/templates/.skills/hustle-interview/SKILL.md +174 -0
- package/templates/CLAUDE-SECTION.md +89 -64
- package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
- package/templates/api-dev-state.json +33 -1
- 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 +367 -58
- package/templates/brand-page/page.tsx +645 -0
- package/templates/component/Component.visual.spec.ts +30 -24
- package/templates/docs/page.tsx +230 -0
- package/templates/eslint-plugin-zod-schema/index.js +446 -0
- package/templates/eslint-plugin-zod-schema/package.json +26 -0
- package/templates/github-workflows/security.yml +274 -0
- package/templates/hustle-build-defaults.json +136 -0
- package/templates/hustle-dev-dashboard/page.tsx +365 -0
- package/templates/page/page.e2e.test.ts +30 -26
- package/templates/performance-budgets.json +63 -5
- package/templates/playwright-report/page.tsx +258 -0
- package/templates/registry.json +279 -3
- package/templates/review-dashboard/page.tsx +510 -0
- package/templates/settings.json +155 -7
- package/templates/test-results/page.tsx +237 -0
- package/templates/typedoc.json +19 -0
- package/templates/ui-showcase/_components/UIShowcase.tsx +48 -1
- package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
- package/templates/ui-showcase/page.tsx +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect } from "react";
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
4
|
|
|
5
5
|
interface ParameterDoc {
|
|
6
6
|
name: string;
|
|
@@ -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,19 @@ 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
|
+
|
|
30
|
+
/** State for each parameter in the interactive builder */
|
|
31
|
+
interface ParamState {
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
value: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
interface APITesterProps {
|
|
21
37
|
id: string;
|
|
22
38
|
endpoint: string;
|
|
@@ -24,6 +40,16 @@ interface APITesterProps {
|
|
|
24
40
|
selectedEndpoint?: string | null;
|
|
25
41
|
schemaPath?: string;
|
|
26
42
|
schema?: SchemaDoc;
|
|
43
|
+
/** Parameters from registry.json endpoints section */
|
|
44
|
+
endpointParams?: ParameterDoc[];
|
|
45
|
+
/** API route file path for reference */
|
|
46
|
+
apiRoute?: string;
|
|
47
|
+
/** Pre-built examples from registry.json */
|
|
48
|
+
examples?: Record<string, EndpointExample>;
|
|
49
|
+
/** Callback to expose submit function to parent */
|
|
50
|
+
onSubmitRef?: (submitFn: () => Promise<void>) => void;
|
|
51
|
+
/** Callback to notify parent of loading state */
|
|
52
|
+
onLoadingChange?: (isLoading: boolean) => void;
|
|
27
53
|
}
|
|
28
54
|
|
|
29
55
|
interface RequestState {
|
|
@@ -80,7 +106,7 @@ const DEFAULT_QUERY_PARAMS: Record<string, Record<string, string>> = {
|
|
|
80
106
|
* - Response display with timing
|
|
81
107
|
* - Audio playback for binary responses
|
|
82
108
|
*
|
|
83
|
-
* Created with Hustle API Dev Tools (v3.
|
|
109
|
+
* Created with Hustle API Dev Tools (v3.12.12)
|
|
84
110
|
*/
|
|
85
111
|
export function APITester({
|
|
86
112
|
id,
|
|
@@ -89,7 +115,63 @@ export function APITester({
|
|
|
89
115
|
selectedEndpoint,
|
|
90
116
|
schemaPath,
|
|
91
117
|
schema,
|
|
118
|
+
endpointParams,
|
|
119
|
+
apiRoute,
|
|
120
|
+
examples,
|
|
121
|
+
onSubmitRef,
|
|
122
|
+
onLoadingChange,
|
|
92
123
|
}: APITesterProps) {
|
|
124
|
+
const [selectedExample, setSelectedExample] = useState<string | null>(null);
|
|
125
|
+
|
|
126
|
+
// Interactive parameter builder state
|
|
127
|
+
const [paramStates, setParamStates] = useState<Record<string, ParamState>>({});
|
|
128
|
+
|
|
129
|
+
// Initialize param states from endpointParams
|
|
130
|
+
const initializeParamStates = useCallback((params: ParameterDoc[] | undefined) => {
|
|
131
|
+
if (!params) return {};
|
|
132
|
+
const states: Record<string, ParamState> = {};
|
|
133
|
+
for (const param of params) {
|
|
134
|
+
// Default value: example > default > empty
|
|
135
|
+
const defaultValue = param.example ||
|
|
136
|
+
(param.default !== undefined ? String(param.default) : "") ||
|
|
137
|
+
(param.enum?.[0] || "");
|
|
138
|
+
|
|
139
|
+
// Required params are enabled by default
|
|
140
|
+
states[param.name] = {
|
|
141
|
+
enabled: param.required || false,
|
|
142
|
+
value: defaultValue,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return states;
|
|
146
|
+
}, []);
|
|
147
|
+
|
|
148
|
+
// Build query string from param states
|
|
149
|
+
const buildQueryFromStates = useCallback((states: Record<string, ParamState>) => {
|
|
150
|
+
const parts: string[] = [];
|
|
151
|
+
for (const [name, state] of Object.entries(states)) {
|
|
152
|
+
if (state.enabled && state.value) {
|
|
153
|
+
parts.push(`${name}=${encodeURIComponent(state.value)}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return parts.join("&");
|
|
157
|
+
}, []);
|
|
158
|
+
|
|
159
|
+
// Update param states from query string (for example buttons)
|
|
160
|
+
const updateStatesFromQuery = useCallback((query: string, params: ParameterDoc[] | undefined) => {
|
|
161
|
+
if (!params) return;
|
|
162
|
+
const searchParams = new URLSearchParams(query);
|
|
163
|
+
const newStates: Record<string, ParamState> = {};
|
|
164
|
+
|
|
165
|
+
for (const param of params) {
|
|
166
|
+
const value = searchParams.get(param.name);
|
|
167
|
+
newStates[param.name] = {
|
|
168
|
+
enabled: value !== null,
|
|
169
|
+
value: value || param.example || (param.default !== undefined ? String(param.default) : "") || (param.enum?.[0] || ""),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
setParamStates(newStates);
|
|
173
|
+
}, []);
|
|
174
|
+
|
|
93
175
|
// Get default body for this API/endpoint
|
|
94
176
|
const getDefaultBody = () => {
|
|
95
177
|
const apiDefaults = DEFAULT_BODIES[id];
|
|
@@ -104,10 +186,30 @@ export function APITester({
|
|
|
104
186
|
|
|
105
187
|
// Get default query params for GET requests
|
|
106
188
|
const getDefaultQueryParams = () => {
|
|
189
|
+
// First check hardcoded defaults
|
|
107
190
|
const apiParams = DEFAULT_QUERY_PARAMS[id];
|
|
108
191
|
if (apiParams && selectedEndpoint) {
|
|
109
192
|
return apiParams[selectedEndpoint] || "";
|
|
110
193
|
}
|
|
194
|
+
|
|
195
|
+
// Build from endpointParams if available
|
|
196
|
+
if (endpointParams && endpointParams.length > 0) {
|
|
197
|
+
const queryParts: string[] = [];
|
|
198
|
+
for (const param of endpointParams) {
|
|
199
|
+
if (param.name === "action") {
|
|
200
|
+
// Add action with the selectedEndpoint or default value
|
|
201
|
+
queryParts.push(`action=${selectedEndpoint || param.default || ""}`);
|
|
202
|
+
} else if (param.required && param.example) {
|
|
203
|
+
// Add required params with example values
|
|
204
|
+
queryParts.push(`${param.name}=${encodeURIComponent(param.example)}`);
|
|
205
|
+
} else if (param.required && param.default !== undefined) {
|
|
206
|
+
// Add required params with defaults
|
|
207
|
+
queryParts.push(`${param.name}=${encodeURIComponent(String(param.default))}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return queryParts.join("&");
|
|
211
|
+
}
|
|
212
|
+
|
|
111
213
|
return "";
|
|
112
214
|
};
|
|
113
215
|
|
|
@@ -132,15 +234,34 @@ export function APITester({
|
|
|
132
234
|
const [isLoading, setIsLoading] = useState(false);
|
|
133
235
|
const [audioUrl, setAudioUrl] = useState<string | null>(null);
|
|
134
236
|
|
|
237
|
+
// Expose submit function to parent for footer button
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
if (onSubmitRef) {
|
|
240
|
+
onSubmitRef(handleSubmit);
|
|
241
|
+
}
|
|
242
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
243
|
+
}, [onSubmitRef, request, endpoint]);
|
|
244
|
+
|
|
245
|
+
// Notify parent of loading state changes
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
if (onLoadingChange) {
|
|
248
|
+
onLoadingChange(isLoading);
|
|
249
|
+
}
|
|
250
|
+
}, [isLoading, onLoadingChange]);
|
|
251
|
+
|
|
135
252
|
// Update defaults when endpoint changes
|
|
136
253
|
useEffect(() => {
|
|
254
|
+
const queryParams = endpointParams || schema?.queryParams;
|
|
255
|
+
const initialStates = initializeParamStates(queryParams);
|
|
256
|
+
setParamStates(initialStates);
|
|
257
|
+
|
|
137
258
|
setRequest((prev) => ({
|
|
138
259
|
...prev,
|
|
139
260
|
method: methods[0] || "POST",
|
|
140
261
|
body: getDefaultBody(),
|
|
141
|
-
queryParams:
|
|
262
|
+
queryParams: buildQueryFromStates(initialStates),
|
|
142
263
|
}));
|
|
143
|
-
// Clear previous response
|
|
264
|
+
// Clear previous response and example selection
|
|
144
265
|
setResponse({
|
|
145
266
|
status: null,
|
|
146
267
|
statusText: "",
|
|
@@ -150,8 +271,17 @@ export function APITester({
|
|
|
150
271
|
contentType: null,
|
|
151
272
|
});
|
|
152
273
|
setAudioUrl(null);
|
|
274
|
+
setSelectedExample(null);
|
|
153
275
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
154
|
-
}, [selectedEndpoint, id]);
|
|
276
|
+
}, [selectedEndpoint, id, endpointParams, examples, schema]);
|
|
277
|
+
|
|
278
|
+
// Auto-update query string when param states change
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
if (Object.keys(paramStates).length > 0) {
|
|
281
|
+
const newQuery = buildQueryFromStates(paramStates);
|
|
282
|
+
setRequest((prev) => ({ ...prev, queryParams: newQuery }));
|
|
283
|
+
}
|
|
284
|
+
}, [paramStates, buildQueryFromStates]);
|
|
155
285
|
|
|
156
286
|
const handleSubmit = async () => {
|
|
157
287
|
setIsLoading(true);
|
|
@@ -298,6 +428,49 @@ export function APITester({
|
|
|
298
428
|
</div>
|
|
299
429
|
</div>
|
|
300
430
|
|
|
431
|
+
{/* Example Selector (for GET requests with examples) */}
|
|
432
|
+
{request.method === "GET" && examples && Object.keys(examples).length > 0 && (
|
|
433
|
+
<div>
|
|
434
|
+
<label className="mb-1 block text-sm font-bold text-black dark:text-white">
|
|
435
|
+
Example Requests
|
|
436
|
+
</label>
|
|
437
|
+
<div className="flex flex-wrap gap-2">
|
|
438
|
+
{Object.entries(examples).map(([key, example]) => (
|
|
439
|
+
<button
|
|
440
|
+
key={key}
|
|
441
|
+
onClick={() => {
|
|
442
|
+
setSelectedExample(key);
|
|
443
|
+
updateStatesFromQuery(example.query, endpointParams || schema?.queryParams);
|
|
444
|
+
}}
|
|
445
|
+
className={`border-2 px-3 py-1.5 text-xs font-medium transition-colors ${
|
|
446
|
+
selectedExample === key
|
|
447
|
+
? "border-[#BA0C2F] bg-[#BA0C2F] text-white"
|
|
448
|
+
: "border-black bg-white text-black hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
449
|
+
}`}
|
|
450
|
+
title={example.description}
|
|
451
|
+
>
|
|
452
|
+
{key.replace(/_/g, " ")}
|
|
453
|
+
</button>
|
|
454
|
+
))}
|
|
455
|
+
</div>
|
|
456
|
+
{selectedExample && examples[selectedExample] && (
|
|
457
|
+
<div className="mt-2 flex items-center gap-2">
|
|
458
|
+
<p className="text-xs text-gray-600 dark:text-gray-400">
|
|
459
|
+
{examples[selectedExample].description}
|
|
460
|
+
</p>
|
|
461
|
+
<button
|
|
462
|
+
onClick={() => {
|
|
463
|
+
navigator.clipboard.writeText(examples[selectedExample].curl);
|
|
464
|
+
}}
|
|
465
|
+
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"
|
|
466
|
+
>
|
|
467
|
+
Copy curl
|
|
468
|
+
</button>
|
|
469
|
+
</div>
|
|
470
|
+
)}
|
|
471
|
+
</div>
|
|
472
|
+
)}
|
|
473
|
+
|
|
301
474
|
{/* Query Parameters (for GET requests) */}
|
|
302
475
|
{request.method === "GET" && (
|
|
303
476
|
<div>
|
|
@@ -307,14 +480,15 @@ export function APITester({
|
|
|
307
480
|
<input
|
|
308
481
|
type="text"
|
|
309
482
|
value={request.queryParams}
|
|
310
|
-
onChange={(e) =>
|
|
311
|
-
|
|
312
|
-
|
|
483
|
+
onChange={(e) => {
|
|
484
|
+
setSelectedExample(null); // Clear example selection when manually editing
|
|
485
|
+
setRequest((prev) => ({ ...prev, queryParams: e.target.value }));
|
|
486
|
+
}}
|
|
313
487
|
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
488
|
placeholder="key1=value1&key2=value2"
|
|
315
489
|
/>
|
|
316
490
|
<p className="mt-1 text-xs text-gray-600 dark:text-gray-400">
|
|
317
|
-
Add query string parameters (without the ?)
|
|
491
|
+
Add query string parameters (without the ?) - or select an example above
|
|
318
492
|
</p>
|
|
319
493
|
</div>
|
|
320
494
|
)}
|
|
@@ -346,14 +520,22 @@ export function APITester({
|
|
|
346
520
|
</div>
|
|
347
521
|
)}
|
|
348
522
|
|
|
349
|
-
{/* Parameter
|
|
350
|
-
{
|
|
351
|
-
<
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
523
|
+
{/* Interactive Parameter Builder (for GET requests with params) */}
|
|
524
|
+
{request.method === "GET" && (endpointParams?.length || schema?.queryParams?.length) ? (
|
|
525
|
+
<InteractiveParamBuilder
|
|
526
|
+
params={endpointParams || schema?.queryParams || []}
|
|
527
|
+
paramStates={paramStates}
|
|
528
|
+
setParamStates={setParamStates}
|
|
355
529
|
/>
|
|
356
|
-
) :
|
|
530
|
+
) : (
|
|
531
|
+
/* Static Parameter Documentation (for non-GET requests) */
|
|
532
|
+
(schema && schema.request?.length) ? (
|
|
533
|
+
<ParameterDocs
|
|
534
|
+
requestParams={schema?.request}
|
|
535
|
+
isGetRequest={false}
|
|
536
|
+
/>
|
|
537
|
+
) : null
|
|
538
|
+
)}
|
|
357
539
|
|
|
358
540
|
{/* Headers */}
|
|
359
541
|
<div>
|
|
@@ -376,41 +558,6 @@ export function APITester({
|
|
|
376
558
|
API keys loaded from .env automatically
|
|
377
559
|
</p>
|
|
378
560
|
</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
561
|
</div>
|
|
415
562
|
|
|
416
563
|
{/* Response Panel */}
|
|
@@ -488,23 +635,170 @@ export function APITester({
|
|
|
488
635
|
);
|
|
489
636
|
}
|
|
490
637
|
|
|
638
|
+
/**
|
|
639
|
+
* Interactive Parameter Builder Component
|
|
640
|
+
* Allows users to check/uncheck params and edit values to build query strings.
|
|
641
|
+
*/
|
|
642
|
+
function InteractiveParamBuilder({
|
|
643
|
+
params,
|
|
644
|
+
paramStates,
|
|
645
|
+
setParamStates,
|
|
646
|
+
}: {
|
|
647
|
+
params: ParameterDoc[];
|
|
648
|
+
paramStates: Record<string, ParamState>;
|
|
649
|
+
setParamStates: React.Dispatch<React.SetStateAction<Record<string, ParamState>>>;
|
|
650
|
+
}) {
|
|
651
|
+
const [isExpanded, setIsExpanded] = useState(true);
|
|
652
|
+
|
|
653
|
+
if (!params.length) return null;
|
|
654
|
+
|
|
655
|
+
const handleToggle = (name: string) => {
|
|
656
|
+
setParamStates((prev) => ({
|
|
657
|
+
...prev,
|
|
658
|
+
[name]: {
|
|
659
|
+
...prev[name],
|
|
660
|
+
enabled: !prev[name]?.enabled,
|
|
661
|
+
},
|
|
662
|
+
}));
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
const handleValueChange = (name: string, value: string) => {
|
|
666
|
+
setParamStates((prev) => ({
|
|
667
|
+
...prev,
|
|
668
|
+
[name]: {
|
|
669
|
+
...prev[name],
|
|
670
|
+
value,
|
|
671
|
+
enabled: true, // Auto-enable when value is changed
|
|
672
|
+
},
|
|
673
|
+
}));
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
return (
|
|
677
|
+
<div className="border-2 border-black dark:border-gray-600">
|
|
678
|
+
<button
|
|
679
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
680
|
+
className="flex w-full items-center justify-between bg-gray-50 px-3 py-2 text-left dark:bg-gray-800"
|
|
681
|
+
>
|
|
682
|
+
<span className="text-sm font-bold text-black dark:text-white">
|
|
683
|
+
Query Parameters Builder
|
|
684
|
+
</span>
|
|
685
|
+
<svg
|
|
686
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
687
|
+
width="16"
|
|
688
|
+
height="16"
|
|
689
|
+
viewBox="0 0 24 24"
|
|
690
|
+
fill="none"
|
|
691
|
+
stroke="currentColor"
|
|
692
|
+
strokeWidth="2"
|
|
693
|
+
strokeLinecap="round"
|
|
694
|
+
strokeLinejoin="round"
|
|
695
|
+
className={`text-gray-500 transition-transform ${isExpanded ? "rotate-180" : ""}`}
|
|
696
|
+
>
|
|
697
|
+
<polyline points="6 9 12 15 18 9" />
|
|
698
|
+
</svg>
|
|
699
|
+
</button>
|
|
700
|
+
|
|
701
|
+
{isExpanded && (
|
|
702
|
+
<div className="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-900">
|
|
703
|
+
{params.map((param) => {
|
|
704
|
+
const state = paramStates[param.name] || { enabled: false, value: "" };
|
|
705
|
+
|
|
706
|
+
return (
|
|
707
|
+
<div key={param.name} className="flex items-start gap-3 px-3 py-2">
|
|
708
|
+
{/* Checkbox */}
|
|
709
|
+
<label className="flex h-8 cursor-pointer items-center">
|
|
710
|
+
<input
|
|
711
|
+
type="checkbox"
|
|
712
|
+
checked={state.enabled}
|
|
713
|
+
onChange={() => handleToggle(param.name)}
|
|
714
|
+
disabled={param.required}
|
|
715
|
+
className="h-4 w-4 cursor-pointer accent-[#BA0C2F] disabled:cursor-not-allowed disabled:opacity-50"
|
|
716
|
+
/>
|
|
717
|
+
</label>
|
|
718
|
+
|
|
719
|
+
{/* Parameter Info */}
|
|
720
|
+
<div className="min-w-0 flex-1">
|
|
721
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
722
|
+
<code className="text-sm font-bold text-[#BA0C2F]">
|
|
723
|
+
{param.name}
|
|
724
|
+
</code>
|
|
725
|
+
<span className="border border-gray-300 bg-gray-100 px-1.5 py-0.5 text-xs text-gray-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400">
|
|
726
|
+
{param.type}
|
|
727
|
+
</span>
|
|
728
|
+
{param.required && (
|
|
729
|
+
<span className="border border-red-300 bg-red-50 px-1.5 py-0.5 text-xs text-red-600 dark:border-red-800 dark:bg-red-900/30 dark:text-red-400">
|
|
730
|
+
required
|
|
731
|
+
</span>
|
|
732
|
+
)}
|
|
733
|
+
</div>
|
|
734
|
+
{param.description && (
|
|
735
|
+
<p className="mt-0.5 text-xs text-gray-600 dark:text-gray-400">
|
|
736
|
+
{param.description}
|
|
737
|
+
</p>
|
|
738
|
+
)}
|
|
739
|
+
</div>
|
|
740
|
+
|
|
741
|
+
{/* Input Control */}
|
|
742
|
+
<div className="w-36 shrink-0">
|
|
743
|
+
{param.enum ? (
|
|
744
|
+
/* Dropdown for enum types */
|
|
745
|
+
<select
|
|
746
|
+
value={state.value}
|
|
747
|
+
onChange={(e) => handleValueChange(param.name, e.target.value)}
|
|
748
|
+
className="w-full border border-gray-300 bg-white px-2 py-1.5 text-sm focus:border-[#BA0C2F] focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
749
|
+
>
|
|
750
|
+
<option value="">-- none --</option>
|
|
751
|
+
{param.enum.map((val) => (
|
|
752
|
+
<option key={val} value={val}>
|
|
753
|
+
{val}
|
|
754
|
+
</option>
|
|
755
|
+
))}
|
|
756
|
+
</select>
|
|
757
|
+
) : param.type === "number" || param.type === "integer" ? (
|
|
758
|
+
/* Number input for numeric types */
|
|
759
|
+
<input
|
|
760
|
+
type="number"
|
|
761
|
+
value={state.value}
|
|
762
|
+
onChange={(e) => handleValueChange(param.name, e.target.value)}
|
|
763
|
+
min={param.min}
|
|
764
|
+
max={param.max}
|
|
765
|
+
placeholder={param.example || (param.default !== undefined ? String(param.default) : "")}
|
|
766
|
+
className="w-full border border-gray-300 bg-white px-2 py-1.5 text-sm focus:border-[#BA0C2F] focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
767
|
+
/>
|
|
768
|
+
) : (
|
|
769
|
+
/* Text input for string types */
|
|
770
|
+
<input
|
|
771
|
+
type="text"
|
|
772
|
+
value={state.value}
|
|
773
|
+
onChange={(e) => handleValueChange(param.name, e.target.value)}
|
|
774
|
+
placeholder={param.example || (param.default !== undefined ? String(param.default) : "")}
|
|
775
|
+
className="w-full border border-gray-300 bg-white px-2 py-1.5 text-sm focus:border-[#BA0C2F] focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
776
|
+
/>
|
|
777
|
+
)}
|
|
778
|
+
</div>
|
|
779
|
+
</div>
|
|
780
|
+
);
|
|
781
|
+
})}
|
|
782
|
+
</div>
|
|
783
|
+
)}
|
|
784
|
+
</div>
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
|
|
491
788
|
/**
|
|
492
789
|
* Parameter Documentation Component
|
|
493
|
-
* Displays request body
|
|
790
|
+
* Displays request body documentation in a collapsible panel (for non-GET requests).
|
|
494
791
|
*/
|
|
495
792
|
function ParameterDocs({
|
|
496
793
|
requestParams,
|
|
497
|
-
queryParams,
|
|
498
794
|
isGetRequest,
|
|
499
795
|
}: {
|
|
500
796
|
requestParams?: ParameterDoc[];
|
|
501
|
-
queryParams?: ParameterDoc[];
|
|
502
797
|
isGetRequest: boolean;
|
|
503
798
|
}) {
|
|
504
799
|
const [isExpanded, setIsExpanded] = useState(true);
|
|
505
800
|
|
|
506
|
-
|
|
507
|
-
if (!paramsToShow?.length) return null;
|
|
801
|
+
if (!requestParams?.length) return null;
|
|
508
802
|
|
|
509
803
|
return (
|
|
510
804
|
<div className="border-2 border-black dark:border-gray-600">
|
|
@@ -513,7 +807,7 @@ function ParameterDocs({
|
|
|
513
807
|
className="flex w-full items-center justify-between bg-gray-50 px-3 py-2 text-left dark:bg-gray-800"
|
|
514
808
|
>
|
|
515
809
|
<span className="text-sm font-bold text-black dark:text-white">
|
|
516
|
-
|
|
810
|
+
Request Body Documentation
|
|
517
811
|
</span>
|
|
518
812
|
<svg
|
|
519
813
|
xmlns="http://www.w3.org/2000/svg"
|
|
@@ -533,7 +827,7 @@ function ParameterDocs({
|
|
|
533
827
|
|
|
534
828
|
{isExpanded && (
|
|
535
829
|
<div className="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-900">
|
|
536
|
-
{
|
|
830
|
+
{requestParams.map((param) => (
|
|
537
831
|
<div key={param.name} className="px-3 py-2">
|
|
538
832
|
<div className="flex items-center gap-2">
|
|
539
833
|
<code className="text-sm font-bold text-[#BA0C2F]">
|
|
@@ -574,6 +868,21 @@ function ParameterDocs({
|
|
|
574
868
|
</code>
|
|
575
869
|
</p>
|
|
576
870
|
)}
|
|
871
|
+
{param.example && (
|
|
872
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
873
|
+
Example:{" "}
|
|
874
|
+
<code className="text-gray-700 dark:text-gray-300">
|
|
875
|
+
{param.example}
|
|
876
|
+
</code>
|
|
877
|
+
</p>
|
|
878
|
+
)}
|
|
879
|
+
{(param.min !== undefined || param.max !== undefined) && (
|
|
880
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
881
|
+
{param.min !== undefined && `Min: ${param.min}`}
|
|
882
|
+
{param.min !== undefined && param.max !== undefined && " · "}
|
|
883
|
+
{param.max !== undefined && `Max: ${param.max}`}
|
|
884
|
+
</p>
|
|
885
|
+
)}
|
|
577
886
|
</div>
|
|
578
887
|
))}
|
|
579
888
|
</div>
|