@hustle-together/api-dev-tools 3.11.1 → 3.12.2
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/agents/code-reviewer.md +170 -0
- package/.claude/agents/docs-generator.md +80 -0
- package/.claude/agents/implementation-reviewer.md +119 -0
- package/.claude/agents/parallel-researcher.md +52 -0
- package/.claude/agents/research-validator.md +116 -0
- package/.claude/agents/schema-generator.md +70 -0
- package/.claude/agents/test-writer.md +104 -0
- package/.claude/api-dev-state.json +305 -56
- package/.claude/commands/README.md +21 -10
- package/.claude/commands/add-command.md +8 -5
- package/.claude/commands/api-create.md +36 -25
- package/.claude/commands/api-env.md +1 -0
- package/.claude/commands/api-interview.md +32 -19
- package/.claude/commands/api-research.md +47 -21
- package/.claude/commands/api-status.md +21 -1
- package/.claude/commands/api-verify.md +14 -13
- package/.claude/commands/beepboop.md +4 -5
- package/.claude/commands/busycommit.md +2 -3
- package/.claude/commands/commit.md +2 -3
- package/.claude/commands/cycle.md +2 -7
- package/.claude/commands/gap.md +2 -3
- package/.claude/commands/green.md +2 -7
- package/.claude/commands/issue.md +3 -8
- package/.claude/commands/ntfy-setup.md +91 -0
- package/.claude/commands/ntfy-test.md +74 -0
- package/.claude/commands/plan.md +2 -3
- package/.claude/commands/pr.md +2 -3
- package/.claude/commands/publish.md +40 -0
- package/.claude/commands/red.md +2 -7
- package/.claude/commands/refactor.md +2 -7
- package/.claude/commands/spike.md +2 -7
- package/.claude/commands/summarize.md +2 -3
- package/.claude/commands/tdd.md +2 -7
- package/.claude/commands/worktree-add.md +208 -216
- package/.claude/commands/worktree-cleanup.md +172 -178
- package/.claude/settings.json +63 -12
- package/.claude/settings.local.json +2 -1
- package/.claude-plugin/marketplace.json +2 -11
- package/.skills/README.md +55 -53
- package/.skills/_shared/settings.json +1 -1
- package/.skills/add-command/SKILL.md +10 -5
- package/.skills/api-create/SKILL.md +146 -35
- package/.skills/api-env/SKILL.md +1 -0
- package/.skills/api-interview/SKILL.md +32 -19
- package/.skills/api-research/SKILL.md +47 -21
- package/.skills/api-status/SKILL.md +21 -1
- package/.skills/api-verify/SKILL.md +14 -13
- package/.skills/beepboop/SKILL.md +6 -5
- package/.skills/busycommit/SKILL.md +4 -3
- package/.skills/commit/SKILL.md +4 -3
- package/.skills/cycle/SKILL.md +4 -7
- package/.skills/gap/SKILL.md +4 -3
- package/.skills/green/SKILL.md +4 -7
- package/.skills/issue/SKILL.md +5 -8
- package/.skills/plan/SKILL.md +4 -3
- package/.skills/pr/SKILL.md +4 -3
- package/.skills/publish/SKILL.md +160 -0
- package/.skills/red/SKILL.md +4 -7
- package/.skills/refactor/SKILL.md +4 -7
- package/.skills/spike/SKILL.md +4 -7
- package/.skills/summarize/SKILL.md +4 -3
- package/.skills/tdd/SKILL.md +4 -7
- package/.skills/update-todos/SKILL.md +22 -0
- package/.skills/worktree-add/SKILL.md +210 -216
- package/.skills/worktree-cleanup/SKILL.md +183 -187
- package/CHANGELOG.md +97 -79
- package/README.md +161 -7142
- package/bin/cli.js +448 -805
- package/commands/README.md +66 -31
- package/commands/add-command.md +8 -5
- package/commands/beepboop.md +4 -5
- package/commands/busycommit.md +2 -3
- package/commands/commit.md +2 -3
- package/commands/cycle.md +2 -7
- package/commands/gap.md +2 -3
- package/commands/green.md +2 -7
- package/commands/hustle-api-continue.md +8 -5
- package/commands/hustle-api-create.md +70 -29
- package/commands/hustle-api-env.md +1 -0
- package/commands/hustle-api-interview.md +32 -19
- package/commands/hustle-api-research.md +47 -21
- package/commands/hustle-api-sessions.md +8 -7
- package/commands/hustle-api-status.md +21 -1
- package/commands/hustle-api-verify.md +14 -13
- package/commands/hustle-combine.md +488 -241
- package/commands/hustle-ui-create-page.md +113 -50
- package/commands/hustle-ui-create.md +179 -26
- package/commands/issue.md +3 -8
- package/commands/plan.md +2 -3
- package/commands/pr.md +2 -3
- package/commands/red.md +2 -7
- package/commands/refactor.md +2 -7
- package/commands/spike.md +2 -7
- package/commands/summarize.md +2 -3
- package/commands/tdd.md +2 -7
- package/commands/worktree-add.md +208 -216
- package/commands/worktree-cleanup.md +172 -178
- package/hooks/api-workflow-check.py +5 -3
- package/hooks/enforce-component-type-confirm.py +97 -0
- package/hooks/lib/__init__.py +1 -0
- package/hooks/lib/greptile.py +355 -0
- package/hooks/lib/ntfy.py +209 -0
- package/hooks/notify-input-needed.py +73 -0
- package/hooks/notify-phase-complete.py +90 -0
- package/hooks/run-code-review.py +246 -0
- package/hooks/track-token-usage.py +121 -0
- package/package.json +13 -3
- package/scripts/collect-test-results.ts +102 -77
- package/scripts/extract-parameters.ts +112 -70
- package/scripts/generate-test-manifest.ts +118 -77
- package/templates/.env.example +57 -0
- package/templates/BRAND_GUIDE.md +92 -52
- package/templates/CLAUDE-SECTION.md +40 -37
- package/templates/SPEC.json +186 -38
- package/templates/api-dev-state.json +33 -4
- package/templates/api-showcase/_components/APICard.tsx +22 -18
- package/templates/api-showcase/_components/APIModal.tsx +110 -64
- package/templates/api-showcase/_components/APIShowcase.tsx +53 -35
- package/templates/api-showcase/_components/APITester.tsx +128 -67
- package/templates/api-showcase/page.tsx +4 -4
- package/templates/api-test/page.tsx +51 -30
- package/templates/api-test/test-structure/route.ts +43 -34
- package/templates/component/Component.stories.tsx +41 -39
- package/templates/component/Component.test.tsx +96 -78
- package/templates/component/Component.tsx +63 -52
- package/templates/component/Component.types.ts +10 -6
- package/templates/component/Component.visual.spec.ts +170 -0
- package/templates/component/index.ts +2 -2
- package/templates/dev-tools/_components/DevToolsLanding.tsx +8 -8
- package/templates/dev-tools/page.tsx +4 -3
- package/templates/mcp-servers.json +30 -2
- package/templates/page/page.e2e.test.ts +56 -48
- package/templates/page/page.tsx +3 -3
- package/templates/shared/HeroHeader.tsx +16 -15
- package/templates/shared/index.ts +1 -1
- package/templates/ui-showcase/_components/PreviewCard.tsx +20 -20
- package/templates/ui-showcase/_components/PreviewModal.tsx +149 -108
- package/templates/ui-showcase/_components/UIShowcase.tsx +43 -35
- package/templates/ui-showcase/page.tsx +4 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect } from
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
4
|
|
|
5
5
|
interface ParameterDoc {
|
|
6
6
|
name: string;
|
|
@@ -45,15 +45,15 @@ interface ResponseState {
|
|
|
45
45
|
// Default request bodies for known APIs
|
|
46
46
|
const DEFAULT_BODIES: Record<string, Record<string, object>> = {
|
|
47
47
|
brandfetch: {
|
|
48
|
-
default: { domain:
|
|
48
|
+
default: { domain: "stripe.com" },
|
|
49
49
|
},
|
|
50
50
|
elevenlabs: {
|
|
51
51
|
tts: {
|
|
52
|
-
text:
|
|
53
|
-
voiceId:
|
|
54
|
-
modelId:
|
|
55
|
-
outputFormat:
|
|
56
|
-
responseFormat:
|
|
52
|
+
text: "Hello, this is a test of the ElevenLabs text-to-speech API.",
|
|
53
|
+
voiceId: "21m00Tcm4TlvDq8ikWAM",
|
|
54
|
+
modelId: "eleven_multilingual_v2",
|
|
55
|
+
outputFormat: "mp3_44100_128",
|
|
56
|
+
responseFormat: "json",
|
|
57
57
|
},
|
|
58
58
|
voices: {},
|
|
59
59
|
models: {},
|
|
@@ -63,8 +63,8 @@ const DEFAULT_BODIES: Record<string, Record<string, object>> = {
|
|
|
63
63
|
// Default query params for GET requests
|
|
64
64
|
const DEFAULT_QUERY_PARAMS: Record<string, Record<string, string>> = {
|
|
65
65
|
elevenlabs: {
|
|
66
|
-
voices:
|
|
67
|
-
models:
|
|
66
|
+
voices: "search=&pageSize=10",
|
|
67
|
+
models: "",
|
|
68
68
|
},
|
|
69
69
|
};
|
|
70
70
|
|
|
@@ -82,12 +82,19 @@ const DEFAULT_QUERY_PARAMS: Record<string, Record<string, string>> = {
|
|
|
82
82
|
*
|
|
83
83
|
* Created with Hustle API Dev Tools (v3.9.2)
|
|
84
84
|
*/
|
|
85
|
-
export function APITester({
|
|
85
|
+
export function APITester({
|
|
86
|
+
id,
|
|
87
|
+
endpoint,
|
|
88
|
+
methods,
|
|
89
|
+
selectedEndpoint,
|
|
90
|
+
schemaPath,
|
|
91
|
+
schema,
|
|
92
|
+
}: APITesterProps) {
|
|
86
93
|
// Get default body for this API/endpoint
|
|
87
94
|
const getDefaultBody = () => {
|
|
88
95
|
const apiDefaults = DEFAULT_BODIES[id];
|
|
89
96
|
if (apiDefaults) {
|
|
90
|
-
const endpointDefaults = apiDefaults[selectedEndpoint ||
|
|
97
|
+
const endpointDefaults = apiDefaults[selectedEndpoint || "default"];
|
|
91
98
|
if (endpointDefaults && Object.keys(endpointDefaults).length > 0) {
|
|
92
99
|
return JSON.stringify(endpointDefaults, null, 2);
|
|
93
100
|
}
|
|
@@ -99,24 +106,24 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
|
|
|
99
106
|
const getDefaultQueryParams = () => {
|
|
100
107
|
const apiParams = DEFAULT_QUERY_PARAMS[id];
|
|
101
108
|
if (apiParams && selectedEndpoint) {
|
|
102
|
-
return apiParams[selectedEndpoint] ||
|
|
109
|
+
return apiParams[selectedEndpoint] || "";
|
|
103
110
|
}
|
|
104
|
-
return
|
|
111
|
+
return "";
|
|
105
112
|
};
|
|
106
113
|
|
|
107
114
|
const [request, setRequest] = useState<RequestState>({
|
|
108
|
-
method: methods[0] ||
|
|
115
|
+
method: methods[0] || "POST",
|
|
109
116
|
body: getDefaultBody(),
|
|
110
117
|
queryParams: getDefaultQueryParams(),
|
|
111
118
|
headers: {
|
|
112
|
-
|
|
119
|
+
"Content-Type": "application/json",
|
|
113
120
|
},
|
|
114
121
|
});
|
|
115
122
|
|
|
116
123
|
const [response, setResponse] = useState<ResponseState>({
|
|
117
124
|
status: null,
|
|
118
|
-
statusText:
|
|
119
|
-
body:
|
|
125
|
+
statusText: "",
|
|
126
|
+
body: "",
|
|
120
127
|
time: null,
|
|
121
128
|
error: null,
|
|
122
129
|
contentType: null,
|
|
@@ -129,26 +136,33 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
|
|
|
129
136
|
useEffect(() => {
|
|
130
137
|
setRequest((prev) => ({
|
|
131
138
|
...prev,
|
|
132
|
-
method: methods[0] ||
|
|
139
|
+
method: methods[0] || "POST",
|
|
133
140
|
body: getDefaultBody(),
|
|
134
141
|
queryParams: getDefaultQueryParams(),
|
|
135
142
|
}));
|
|
136
143
|
// Clear previous response
|
|
137
144
|
setResponse({
|
|
138
145
|
status: null,
|
|
139
|
-
statusText:
|
|
140
|
-
body:
|
|
146
|
+
statusText: "",
|
|
147
|
+
body: "",
|
|
141
148
|
time: null,
|
|
142
149
|
error: null,
|
|
143
150
|
contentType: null,
|
|
144
151
|
});
|
|
145
152
|
setAudioUrl(null);
|
|
146
|
-
|
|
153
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
147
154
|
}, [selectedEndpoint, id]);
|
|
148
155
|
|
|
149
156
|
const handleSubmit = async () => {
|
|
150
157
|
setIsLoading(true);
|
|
151
|
-
setResponse({
|
|
158
|
+
setResponse({
|
|
159
|
+
status: null,
|
|
160
|
+
statusText: "",
|
|
161
|
+
body: "",
|
|
162
|
+
time: null,
|
|
163
|
+
error: null,
|
|
164
|
+
contentType: null,
|
|
165
|
+
});
|
|
152
166
|
setAudioUrl(null);
|
|
153
167
|
|
|
154
168
|
const startTime = performance.now();
|
|
@@ -156,7 +170,7 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
|
|
|
156
170
|
try {
|
|
157
171
|
// Build URL with query params for GET
|
|
158
172
|
let url = endpoint;
|
|
159
|
-
if (request.method ===
|
|
173
|
+
if (request.method === "GET" && request.queryParams.trim()) {
|
|
160
174
|
url = `${endpoint}?${request.queryParams}`;
|
|
161
175
|
}
|
|
162
176
|
|
|
@@ -166,31 +180,34 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
|
|
|
166
180
|
};
|
|
167
181
|
|
|
168
182
|
// Add body for non-GET requests
|
|
169
|
-
if (request.method !==
|
|
183
|
+
if (request.method !== "GET" && request.body.trim()) {
|
|
170
184
|
fetchOptions.body = request.body;
|
|
171
185
|
}
|
|
172
186
|
|
|
173
187
|
const res = await fetch(url, fetchOptions);
|
|
174
188
|
const endTime = performance.now();
|
|
175
189
|
|
|
176
|
-
const contentType = res.headers.get(
|
|
177
|
-
let responseBody =
|
|
190
|
+
const contentType = res.headers.get("content-type") || "";
|
|
191
|
+
let responseBody = "";
|
|
178
192
|
|
|
179
193
|
// Handle different content types
|
|
180
|
-
if (
|
|
194
|
+
if (
|
|
195
|
+
contentType.includes("audio/") ||
|
|
196
|
+
contentType.includes("application/octet-stream")
|
|
197
|
+
) {
|
|
181
198
|
// Binary audio response
|
|
182
199
|
const blob = await res.blob();
|
|
183
200
|
const url = URL.createObjectURL(blob);
|
|
184
201
|
setAudioUrl(url);
|
|
185
202
|
responseBody = `[Audio Response - ${blob.size} bytes]\nContent-Type: ${contentType}`;
|
|
186
|
-
} else if (contentType.includes(
|
|
203
|
+
} else if (contentType.includes("application/json")) {
|
|
187
204
|
const json = await res.json();
|
|
188
205
|
responseBody = JSON.stringify(json, null, 2);
|
|
189
206
|
|
|
190
207
|
// Check if JSON contains base64 audio
|
|
191
|
-
if (json.audio && typeof json.audio ===
|
|
208
|
+
if (json.audio && typeof json.audio === "string") {
|
|
192
209
|
try {
|
|
193
|
-
const format = json.format ||
|
|
210
|
+
const format = json.format || "mp3";
|
|
194
211
|
const audioData = atob(json.audio);
|
|
195
212
|
const bytes = new Uint8Array(audioData.length);
|
|
196
213
|
for (let i = 0; i < audioData.length; i++) {
|
|
@@ -219,10 +236,11 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
|
|
|
219
236
|
const endTime = performance.now();
|
|
220
237
|
setResponse({
|
|
221
238
|
status: null,
|
|
222
|
-
statusText:
|
|
223
|
-
body:
|
|
239
|
+
statusText: "",
|
|
240
|
+
body: "",
|
|
224
241
|
time: Math.round(endTime - startTime),
|
|
225
|
-
error:
|
|
242
|
+
error:
|
|
243
|
+
error instanceof Error ? error.message : "Unknown error occurred",
|
|
226
244
|
contentType: null,
|
|
227
245
|
});
|
|
228
246
|
} finally {
|
|
@@ -231,22 +249,26 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
|
|
|
231
249
|
};
|
|
232
250
|
|
|
233
251
|
const getStatusColor = (status: number | null) => {
|
|
234
|
-
if (!status) return
|
|
235
|
-
if (status >= 200 && status < 300) return
|
|
236
|
-
if (status >= 400 && status < 500) return
|
|
237
|
-
if (status >= 500) return
|
|
238
|
-
return
|
|
252
|
+
if (!status) return "text-gray-500";
|
|
253
|
+
if (status >= 200 && status < 300) return "text-green-500";
|
|
254
|
+
if (status >= 400 && status < 500) return "text-yellow-500";
|
|
255
|
+
if (status >= 500) return "text-red-500";
|
|
256
|
+
return "text-gray-500";
|
|
239
257
|
};
|
|
240
258
|
|
|
241
259
|
return (
|
|
242
260
|
<div className="grid gap-6 lg:grid-cols-2">
|
|
243
261
|
{/* Request Panel */}
|
|
244
262
|
<div className="space-y-4">
|
|
245
|
-
<h3 className="text-lg font-bold text-black dark:text-white">
|
|
263
|
+
<h3 className="text-lg font-bold text-black dark:text-white">
|
|
264
|
+
Request
|
|
265
|
+
</h3>
|
|
246
266
|
|
|
247
267
|
{/* Method Selection */}
|
|
248
268
|
<div>
|
|
249
|
-
<label className="mb-1 block text-sm font-bold text-black dark:text-white">
|
|
269
|
+
<label className="mb-1 block text-sm font-bold text-black dark:text-white">
|
|
270
|
+
Method
|
|
271
|
+
</label>
|
|
250
272
|
<div className="flex gap-2">
|
|
251
273
|
{methods.map((method) => (
|
|
252
274
|
<button
|
|
@@ -254,8 +276,8 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
|
|
|
254
276
|
onClick={() => setRequest((prev) => ({ ...prev, method }))}
|
|
255
277
|
className={`border-2 px-4 py-2 text-sm font-medium transition-colors ${
|
|
256
278
|
request.method === method
|
|
257
|
-
?
|
|
258
|
-
:
|
|
279
|
+
? "border-[#BA0C2F] bg-[#BA0C2F] text-white"
|
|
280
|
+
: "border-black bg-white text-black hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
259
281
|
}`}
|
|
260
282
|
>
|
|
261
283
|
{method}
|
|
@@ -266,20 +288,28 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
|
|
|
266
288
|
|
|
267
289
|
{/* Endpoint Display */}
|
|
268
290
|
<div>
|
|
269
|
-
<label className="mb-1 block text-sm font-bold text-black dark:text-white">
|
|
291
|
+
<label className="mb-1 block text-sm font-bold text-black dark:text-white">
|
|
292
|
+
Endpoint
|
|
293
|
+
</label>
|
|
270
294
|
<div className="flex items-center border-2 border-black bg-gray-50 px-3 py-2 dark:border-gray-600 dark:bg-gray-800">
|
|
271
|
-
<span className="font-mono text-sm text-gray-700 dark:text-gray-300">
|
|
295
|
+
<span className="font-mono text-sm text-gray-700 dark:text-gray-300">
|
|
296
|
+
{endpoint}
|
|
297
|
+
</span>
|
|
272
298
|
</div>
|
|
273
299
|
</div>
|
|
274
300
|
|
|
275
301
|
{/* Query Parameters (for GET requests) */}
|
|
276
|
-
{request.method ===
|
|
302
|
+
{request.method === "GET" && (
|
|
277
303
|
<div>
|
|
278
|
-
<label className="mb-1 block text-sm font-bold text-black dark:text-white">
|
|
304
|
+
<label className="mb-1 block text-sm font-bold text-black dark:text-white">
|
|
305
|
+
Query Parameters
|
|
306
|
+
</label>
|
|
279
307
|
<input
|
|
280
308
|
type="text"
|
|
281
309
|
value={request.queryParams}
|
|
282
|
-
onChange={(e) =>
|
|
310
|
+
onChange={(e) =>
|
|
311
|
+
setRequest((prev) => ({ ...prev, queryParams: e.target.value }))
|
|
312
|
+
}
|
|
283
313
|
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"
|
|
284
314
|
placeholder="key1=value1&key2=value2"
|
|
285
315
|
/>
|
|
@@ -290,12 +320,16 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
|
|
|
290
320
|
)}
|
|
291
321
|
|
|
292
322
|
{/* Body Editor (hide for GET) */}
|
|
293
|
-
{request.method !==
|
|
323
|
+
{request.method !== "GET" && (
|
|
294
324
|
<div>
|
|
295
325
|
<div className="mb-1 flex items-center justify-between">
|
|
296
|
-
<label className="block text-sm font-bold text-black dark:text-white">
|
|
326
|
+
<label className="block text-sm font-bold text-black dark:text-white">
|
|
327
|
+
Body (JSON)
|
|
328
|
+
</label>
|
|
297
329
|
<button
|
|
298
|
-
onClick={() =>
|
|
330
|
+
onClick={() =>
|
|
331
|
+
setRequest((prev) => ({ ...prev, body: getDefaultBody() }))
|
|
332
|
+
}
|
|
299
333
|
className="text-xs text-gray-600 hover:text-[#BA0C2F] dark:text-gray-400"
|
|
300
334
|
>
|
|
301
335
|
Reset to defaults
|
|
@@ -303,7 +337,9 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
|
|
|
303
337
|
</div>
|
|
304
338
|
<textarea
|
|
305
339
|
value={request.body}
|
|
306
|
-
onChange={(e) =>
|
|
340
|
+
onChange={(e) =>
|
|
341
|
+
setRequest((prev) => ({ ...prev, body: e.target.value }))
|
|
342
|
+
}
|
|
307
343
|
className="h-48 w-full border-2 border-black bg-white p-3 font-mono text-sm focus:border-[#BA0C2F] focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
308
344
|
placeholder='{"key": "value"}'
|
|
309
345
|
/>
|
|
@@ -315,18 +351,24 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
|
|
|
315
351
|
<ParameterDocs
|
|
316
352
|
requestParams={schema.request}
|
|
317
353
|
queryParams={schema.queryParams}
|
|
318
|
-
isGetRequest={request.method ===
|
|
354
|
+
isGetRequest={request.method === "GET"}
|
|
319
355
|
/>
|
|
320
356
|
) : null}
|
|
321
357
|
|
|
322
358
|
{/* Headers */}
|
|
323
359
|
<div>
|
|
324
|
-
<label className="mb-1 block text-sm font-bold text-black dark:text-white">
|
|
360
|
+
<label className="mb-1 block text-sm font-bold text-black dark:text-white">
|
|
361
|
+
Headers
|
|
362
|
+
</label>
|
|
325
363
|
<div className="border-2 border-black bg-gray-50 p-3 dark:border-gray-600 dark:bg-gray-800">
|
|
326
364
|
{Object.entries(request.headers).map(([key, value]) => (
|
|
327
365
|
<div key={key} className="flex items-center gap-2 text-sm">
|
|
328
|
-
<span className="font-bold text-black dark:text-white">
|
|
329
|
-
|
|
366
|
+
<span className="font-bold text-black dark:text-white">
|
|
367
|
+
{key}:
|
|
368
|
+
</span>
|
|
369
|
+
<span className="text-gray-600 dark:text-gray-400">
|
|
370
|
+
{value}
|
|
371
|
+
</span>
|
|
330
372
|
</div>
|
|
331
373
|
))}
|
|
332
374
|
</div>
|
|
@@ -374,33 +416,45 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
|
|
|
374
416
|
{/* Response Panel */}
|
|
375
417
|
<div className="space-y-4">
|
|
376
418
|
<div className="flex items-center justify-between">
|
|
377
|
-
<h3 className="text-lg font-bold text-black dark:text-white">
|
|
419
|
+
<h3 className="text-lg font-bold text-black dark:text-white">
|
|
420
|
+
Response
|
|
421
|
+
</h3>
|
|
378
422
|
{response.time !== null && (
|
|
379
|
-
<span className="text-sm text-gray-600 dark:text-gray-400">
|
|
423
|
+
<span className="text-sm text-gray-600 dark:text-gray-400">
|
|
424
|
+
{response.time}ms
|
|
425
|
+
</span>
|
|
380
426
|
)}
|
|
381
427
|
</div>
|
|
382
428
|
|
|
383
429
|
{/* Status */}
|
|
384
430
|
{response.status !== null && (
|
|
385
431
|
<div className="flex items-center gap-2">
|
|
386
|
-
<span
|
|
432
|
+
<span
|
|
433
|
+
className={`text-2xl font-bold ${getStatusColor(response.status)}`}
|
|
434
|
+
>
|
|
387
435
|
{response.status}
|
|
388
436
|
</span>
|
|
389
|
-
<span className="text-gray-600 dark:text-gray-400">
|
|
437
|
+
<span className="text-gray-600 dark:text-gray-400">
|
|
438
|
+
{response.statusText}
|
|
439
|
+
</span>
|
|
390
440
|
</div>
|
|
391
441
|
)}
|
|
392
442
|
|
|
393
443
|
{/* Error */}
|
|
394
444
|
{response.error && (
|
|
395
445
|
<div className="border-2 border-red-600 bg-red-50 p-4 dark:bg-red-900/20">
|
|
396
|
-
<p className="text-sm text-red-800 dark:text-red-300">
|
|
446
|
+
<p className="text-sm text-red-800 dark:text-red-300">
|
|
447
|
+
{response.error}
|
|
448
|
+
</p>
|
|
397
449
|
</div>
|
|
398
450
|
)}
|
|
399
451
|
|
|
400
452
|
{/* Audio Player */}
|
|
401
453
|
{audioUrl && (
|
|
402
454
|
<div className="border-2 border-black bg-gray-50 p-4 dark:border-gray-600 dark:bg-gray-800">
|
|
403
|
-
<p className="mb-2 text-sm font-bold text-black dark:text-white">
|
|
455
|
+
<p className="mb-2 text-sm font-bold text-black dark:text-white">
|
|
456
|
+
Audio Response
|
|
457
|
+
</p>
|
|
404
458
|
<audio controls className="w-full" src={audioUrl}>
|
|
405
459
|
Your browser does not support the audio element.
|
|
406
460
|
</audio>
|
|
@@ -423,7 +477,9 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
|
|
|
423
477
|
) : (
|
|
424
478
|
<div className="flex h-48 items-center justify-center border-2 border-dashed border-black dark:border-gray-600">
|
|
425
479
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
426
|
-
{isLoading
|
|
480
|
+
{isLoading
|
|
481
|
+
? "Waiting for response..."
|
|
482
|
+
: "Send a request to see the response"}
|
|
427
483
|
</p>
|
|
428
484
|
</div>
|
|
429
485
|
)}
|
|
@@ -457,7 +513,7 @@ function ParameterDocs({
|
|
|
457
513
|
className="flex w-full items-center justify-between bg-gray-50 px-3 py-2 text-left dark:bg-gray-800"
|
|
458
514
|
>
|
|
459
515
|
<span className="text-sm font-bold text-black dark:text-white">
|
|
460
|
-
{isGetRequest ?
|
|
516
|
+
{isGetRequest ? "Query Parameters" : "Request Body"} Documentation
|
|
461
517
|
</span>
|
|
462
518
|
<svg
|
|
463
519
|
xmlns="http://www.w3.org/2000/svg"
|
|
@@ -469,7 +525,7 @@ function ParameterDocs({
|
|
|
469
525
|
strokeWidth="2"
|
|
470
526
|
strokeLinecap="round"
|
|
471
527
|
strokeLinejoin="round"
|
|
472
|
-
className={`text-gray-500 transition-transform ${isExpanded ?
|
|
528
|
+
className={`text-gray-500 transition-transform ${isExpanded ? "rotate-180" : ""}`}
|
|
473
529
|
>
|
|
474
530
|
<polyline points="6 9 12 15 18 9" />
|
|
475
531
|
</svg>
|
|
@@ -480,7 +536,9 @@ function ParameterDocs({
|
|
|
480
536
|
{paramsToShow.map((param) => (
|
|
481
537
|
<div key={param.name} className="px-3 py-2">
|
|
482
538
|
<div className="flex items-center gap-2">
|
|
483
|
-
<code className="text-sm font-bold text-[#BA0C2F]">
|
|
539
|
+
<code className="text-sm font-bold text-[#BA0C2F]">
|
|
540
|
+
{param.name}
|
|
541
|
+
</code>
|
|
484
542
|
<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">
|
|
485
543
|
{param.type}
|
|
486
544
|
</span>
|
|
@@ -510,7 +568,10 @@ function ParameterDocs({
|
|
|
510
568
|
)}
|
|
511
569
|
{param.default !== undefined && (
|
|
512
570
|
<p className="mt-1 text-xs text-gray-500">
|
|
513
|
-
Default:
|
|
571
|
+
Default:{" "}
|
|
572
|
+
<code className="text-gray-700 dark:text-gray-300">
|
|
573
|
+
{String(param.default)}
|
|
574
|
+
</code>
|
|
514
575
|
</p>
|
|
515
576
|
)}
|
|
516
577
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
|
-
import { HeroHeader } from
|
|
4
|
-
import { APIShowcase } from
|
|
3
|
+
import { HeroHeader } from "../shared/HeroHeader";
|
|
4
|
+
import { APIShowcase } from "./_components/APIShowcase";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* API Showcase Page
|
|
@@ -27,7 +27,7 @@ export default function APIShowcasePage() {
|
|
|
27
27
|
badge="API Documentation"
|
|
28
28
|
description={
|
|
29
29
|
<>
|
|
30
|
-
Interactive testing and documentation for all{
|
|
30
|
+
Interactive testing and documentation for all{" "}
|
|
31
31
|
<strong>Hustle</strong> API endpoints.
|
|
32
32
|
</>
|
|
33
33
|
}
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* @generated by @hustle-together/api-dev-tools v3.0
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
"use client";
|
|
11
11
|
|
|
12
|
-
import { useEffect, useState } from
|
|
12
|
+
import { useEffect, useState } from "react";
|
|
13
13
|
|
|
14
14
|
// ============================================
|
|
15
15
|
// Types (mirror the API types)
|
|
@@ -18,7 +18,7 @@ import { useEffect, useState } from 'react';
|
|
|
18
18
|
interface TestCase {
|
|
19
19
|
name: string;
|
|
20
20
|
line: number;
|
|
21
|
-
status:
|
|
21
|
+
status: "pending" | "passed" | "failed" | "skipped";
|
|
22
22
|
duration?: number;
|
|
23
23
|
error?: string;
|
|
24
24
|
}
|
|
@@ -53,29 +53,37 @@ interface TestStructure {
|
|
|
53
53
|
// Components
|
|
54
54
|
// ============================================
|
|
55
55
|
|
|
56
|
-
function StatusBadge({ status }: { status: TestCase[
|
|
56
|
+
function StatusBadge({ status }: { status: TestCase["status"] }) {
|
|
57
57
|
const colors = {
|
|
58
|
-
pending:
|
|
59
|
-
passed:
|
|
60
|
-
failed:
|
|
61
|
-
skipped:
|
|
58
|
+
pending: "bg-gray-500",
|
|
59
|
+
passed: "bg-green-500",
|
|
60
|
+
failed: "bg-red-500",
|
|
61
|
+
skipped: "bg-yellow-500",
|
|
62
62
|
};
|
|
63
63
|
|
|
64
64
|
const icons = {
|
|
65
|
-
pending:
|
|
66
|
-
passed:
|
|
67
|
-
failed:
|
|
68
|
-
skipped:
|
|
65
|
+
pending: "○",
|
|
66
|
+
passed: "✓",
|
|
67
|
+
failed: "✗",
|
|
68
|
+
skipped: "⊘",
|
|
69
69
|
};
|
|
70
70
|
|
|
71
71
|
return (
|
|
72
|
-
<span
|
|
72
|
+
<span
|
|
73
|
+
className={`${colors[status]} text-white text-xs px-2 py-0.5 rounded font-mono`}
|
|
74
|
+
>
|
|
73
75
|
{icons[status]} {status}
|
|
74
76
|
</span>
|
|
75
77
|
);
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
function TestCaseItem({
|
|
80
|
+
function TestCaseItem({
|
|
81
|
+
test,
|
|
82
|
+
filePath,
|
|
83
|
+
}: {
|
|
84
|
+
test: TestCase;
|
|
85
|
+
filePath: string;
|
|
86
|
+
}) {
|
|
79
87
|
return (
|
|
80
88
|
<div className="flex items-center gap-3 py-2 px-3 hover:bg-gray-800/50 rounded group">
|
|
81
89
|
<StatusBadge status={test.status} />
|
|
@@ -93,22 +101,24 @@ function TestCaseItem({ test, filePath }: { test: TestCase; filePath: string })
|
|
|
93
101
|
function TestGroupItem({
|
|
94
102
|
group,
|
|
95
103
|
filePath,
|
|
96
|
-
depth = 0
|
|
104
|
+
depth = 0,
|
|
97
105
|
}: {
|
|
98
106
|
group: TestGroup;
|
|
99
107
|
filePath: string;
|
|
100
108
|
depth?: number;
|
|
101
109
|
}) {
|
|
102
110
|
const [expanded, setExpanded] = useState(true);
|
|
103
|
-
const totalTests =
|
|
111
|
+
const totalTests =
|
|
112
|
+
group.tests.length +
|
|
113
|
+
group.groups.reduce((sum, g) => sum + g.tests.length, 0);
|
|
104
114
|
|
|
105
115
|
return (
|
|
106
|
-
<div className={`${depth > 0 ?
|
|
116
|
+
<div className={`${depth > 0 ? "ml-4 border-l border-gray-700 pl-3" : ""}`}>
|
|
107
117
|
<button
|
|
108
118
|
onClick={() => setExpanded(!expanded)}
|
|
109
119
|
className="flex items-center gap-2 w-full text-left py-2 px-2 hover:bg-gray-800/30 rounded"
|
|
110
120
|
>
|
|
111
|
-
<span className="text-gray-500">{expanded ?
|
|
121
|
+
<span className="text-gray-500">{expanded ? "▼" : "▶"}</span>
|
|
112
122
|
<span className="font-medium text-white">{group.name}</span>
|
|
113
123
|
<span className="text-gray-500 text-sm">({totalTests} tests)</span>
|
|
114
124
|
</button>
|
|
@@ -135,9 +145,10 @@ function TestGroupItem({
|
|
|
135
145
|
function TestFeatureCard({ feature }: { feature: TestFeature }) {
|
|
136
146
|
const [expanded, setExpanded] = useState(true);
|
|
137
147
|
|
|
138
|
-
const passRate =
|
|
139
|
-
|
|
140
|
-
|
|
148
|
+
const passRate =
|
|
149
|
+
feature.totalTests > 0
|
|
150
|
+
? Math.round((feature.passedTests / feature.totalTests) * 100)
|
|
151
|
+
: 0;
|
|
141
152
|
|
|
142
153
|
return (
|
|
143
154
|
<div className="bg-gray-900 border border-gray-800 rounded-lg overflow-hidden">
|
|
@@ -146,7 +157,7 @@ function TestFeatureCard({ feature }: { feature: TestFeature }) {
|
|
|
146
157
|
className="w-full flex items-center justify-between p-4 hover:bg-gray-800/50"
|
|
147
158
|
>
|
|
148
159
|
<div className="flex items-center gap-3">
|
|
149
|
-
<span className="text-gray-500">{expanded ?
|
|
160
|
+
<span className="text-gray-500">{expanded ? "▼" : "▶"}</span>
|
|
150
161
|
<div>
|
|
151
162
|
<h3 className="font-mono text-white">{feature.file}</h3>
|
|
152
163
|
<p className="text-gray-500 text-sm">{feature.relativePath}</p>
|
|
@@ -159,7 +170,9 @@ function TestFeatureCard({ feature }: { feature: TestFeature }) {
|
|
|
159
170
|
<span className="text-red-400">{feature.failedTests} failed</span>
|
|
160
171
|
)}
|
|
161
172
|
{feature.skippedTests > 0 && (
|
|
162
|
-
<span className="text-yellow-400">
|
|
173
|
+
<span className="text-yellow-400">
|
|
174
|
+
{feature.skippedTests} skipped
|
|
175
|
+
</span>
|
|
163
176
|
)}
|
|
164
177
|
</div>
|
|
165
178
|
<div className="w-20 h-2 bg-gray-700 rounded-full overflow-hidden">
|
|
@@ -190,19 +203,27 @@ function SummaryStats({ structure }: { structure: TestStructure }) {
|
|
|
190
203
|
return (
|
|
191
204
|
<div className="grid grid-cols-4 gap-4 mb-6">
|
|
192
205
|
<div className="bg-gray-900 border border-gray-800 rounded-lg p-4">
|
|
193
|
-
<div className="text-3xl font-bold text-white">
|
|
206
|
+
<div className="text-3xl font-bold text-white">
|
|
207
|
+
{structure.totalTests}
|
|
208
|
+
</div>
|
|
194
209
|
<div className="text-gray-500 text-sm">Total Tests</div>
|
|
195
210
|
</div>
|
|
196
211
|
<div className="bg-gray-900 border border-green-900 rounded-lg p-4">
|
|
197
|
-
<div className="text-3xl font-bold text-green-400">
|
|
212
|
+
<div className="text-3xl font-bold text-green-400">
|
|
213
|
+
{structure.passedTests}
|
|
214
|
+
</div>
|
|
198
215
|
<div className="text-gray-500 text-sm">Passed</div>
|
|
199
216
|
</div>
|
|
200
217
|
<div className="bg-gray-900 border border-red-900 rounded-lg p-4">
|
|
201
|
-
<div className="text-3xl font-bold text-red-400">
|
|
218
|
+
<div className="text-3xl font-bold text-red-400">
|
|
219
|
+
{structure.failedTests}
|
|
220
|
+
</div>
|
|
202
221
|
<div className="text-gray-500 text-sm">Failed</div>
|
|
203
222
|
</div>
|
|
204
223
|
<div className="bg-gray-900 border border-yellow-900 rounded-lg p-4">
|
|
205
|
-
<div className="text-3xl font-bold text-yellow-400">
|
|
224
|
+
<div className="text-3xl font-bold text-yellow-400">
|
|
225
|
+
{structure.skippedTests}
|
|
226
|
+
</div>
|
|
206
227
|
<div className="text-gray-500 text-sm">Skipped</div>
|
|
207
228
|
</div>
|
|
208
229
|
</div>
|
|
@@ -222,7 +243,7 @@ export default function ApiTestPage() {
|
|
|
222
243
|
setLoading(true);
|
|
223
244
|
setError(null);
|
|
224
245
|
try {
|
|
225
|
-
const response = await fetch(
|
|
246
|
+
const response = await fetch("/api/test-structure");
|
|
226
247
|
if (!response.ok) {
|
|
227
248
|
throw new Error(`Failed to fetch: ${response.status}`);
|
|
228
249
|
}
|
|
@@ -264,7 +285,7 @@ export default function ApiTestPage() {
|
|
|
264
285
|
disabled={loading}
|
|
265
286
|
className="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg text-sm font-medium transition-colors disabled:opacity-50"
|
|
266
287
|
>
|
|
267
|
-
{loading ?
|
|
288
|
+
{loading ? "Loading..." : "Refresh"}
|
|
268
289
|
</button>
|
|
269
290
|
</div>
|
|
270
291
|
</div>
|
|
@@ -304,7 +325,7 @@ export default function ApiTestPage() {
|
|
|
304
325
|
</div>
|
|
305
326
|
|
|
306
327
|
<div className="mt-6 text-center text-gray-600 text-sm">
|
|
307
|
-
Parsed at {new Date(structure.parsedAt).toLocaleString()} •{
|
|
328
|
+
Parsed at {new Date(structure.parsedAt).toLocaleString()} •{" "}
|
|
308
329
|
{structure.features.length} test files
|
|
309
330
|
</div>
|
|
310
331
|
</>
|