@hustle-together/api-dev-tools 3.6.5 → 3.9.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/README.md +5307 -258
- package/bin/cli.js +348 -20
- package/commands/README.md +459 -71
- package/commands/hustle-api-continue.md +158 -0
- package/commands/{api-create.md → hustle-api-create.md} +22 -2
- package/commands/{api-env.md → hustle-api-env.md} +4 -4
- package/commands/{api-interview.md → hustle-api-interview.md} +1 -1
- package/commands/{api-research.md → hustle-api-research.md} +3 -3
- package/commands/hustle-api-sessions.md +149 -0
- package/commands/{api-status.md → hustle-api-status.md} +16 -16
- package/commands/{api-verify.md → hustle-api-verify.md} +2 -2
- package/commands/hustle-combine.md +763 -0
- package/commands/hustle-ui-create.md +825 -0
- package/hooks/api-workflow-check.py +385 -19
- package/hooks/cache-research.py +337 -0
- package/hooks/check-playwright-setup.py +103 -0
- package/hooks/check-storybook-setup.py +81 -0
- package/hooks/detect-interruption.py +165 -0
- package/hooks/enforce-brand-guide.py +131 -0
- package/hooks/enforce-documentation.py +60 -8
- package/hooks/enforce-freshness.py +184 -0
- package/hooks/enforce-questions-sourced.py +146 -0
- package/hooks/enforce-schema-from-interview.py +248 -0
- package/hooks/enforce-ui-disambiguation.py +108 -0
- package/hooks/enforce-ui-interview.py +130 -0
- package/hooks/generate-manifest-entry.py +981 -0
- package/hooks/session-logger.py +297 -0
- package/hooks/session-startup.py +65 -10
- package/hooks/track-scope-coverage.py +220 -0
- package/hooks/track-tool-use.py +81 -1
- package/hooks/update-api-showcase.py +149 -0
- package/hooks/update-registry.py +352 -0
- package/hooks/update-ui-showcase.py +148 -0
- package/package.json +8 -2
- package/templates/BRAND_GUIDE.md +299 -0
- package/templates/CLAUDE-SECTION.md +56 -24
- package/templates/SPEC.json +640 -0
- package/templates/api-dev-state.json +179 -161
- package/templates/api-showcase/APICard.tsx +153 -0
- package/templates/api-showcase/APIModal.tsx +375 -0
- package/templates/api-showcase/APIShowcase.tsx +231 -0
- package/templates/api-showcase/APITester.tsx +522 -0
- package/templates/api-showcase/page.tsx +41 -0
- package/templates/component/Component.stories.tsx +172 -0
- package/templates/component/Component.test.tsx +237 -0
- package/templates/component/Component.tsx +86 -0
- package/templates/component/Component.types.ts +55 -0
- package/templates/component/index.ts +15 -0
- package/templates/dev-tools/_components/DevToolsLanding.tsx +320 -0
- package/templates/dev-tools/page.tsx +10 -0
- package/templates/page/page.e2e.test.ts +218 -0
- package/templates/page/page.tsx +42 -0
- package/templates/performance-budgets.json +58 -0
- package/templates/registry.json +13 -0
- package/templates/settings.json +74 -0
- package/templates/shared/HeroHeader.tsx +261 -0
- package/templates/shared/index.ts +1 -0
- package/templates/ui-showcase/PreviewCard.tsx +315 -0
- package/templates/ui-showcase/PreviewModal.tsx +676 -0
- package/templates/ui-showcase/UIShowcase.tsx +262 -0
- package/templates/ui-showcase/page.tsx +26 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useCallback, useState } from 'react';
|
|
4
|
+
import { APITester } from './APITester';
|
|
5
|
+
|
|
6
|
+
interface RegistryAPI {
|
|
7
|
+
name: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
route: string;
|
|
10
|
+
schemas?: string;
|
|
11
|
+
tests?: string;
|
|
12
|
+
methods?: string[];
|
|
13
|
+
created_at?: string;
|
|
14
|
+
status?: string;
|
|
15
|
+
combines?: string[];
|
|
16
|
+
flow_type?: string;
|
|
17
|
+
endpoints?: Record<string, {
|
|
18
|
+
methods: string[];
|
|
19
|
+
description?: string;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface APIModalProps {
|
|
24
|
+
id: string;
|
|
25
|
+
type: 'api' | 'combined';
|
|
26
|
+
data: RegistryAPI;
|
|
27
|
+
onClose: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* API Modal Component
|
|
32
|
+
*
|
|
33
|
+
* Full-screen modal for API documentation and interactive testing.
|
|
34
|
+
* Includes:
|
|
35
|
+
* - Endpoint details
|
|
36
|
+
* - Multi-endpoint selector
|
|
37
|
+
* - Interactive "Try It" form
|
|
38
|
+
* - Request/response display
|
|
39
|
+
* - Curl example generation
|
|
40
|
+
*
|
|
41
|
+
* Created with Hustle API Dev Tools (v3.9.2)
|
|
42
|
+
*/
|
|
43
|
+
export function APIModal({ id, type, data, onClose }: APIModalProps) {
|
|
44
|
+
const [activeTab, setActiveTab] = useState<'try-it' | 'docs' | 'curl'>('try-it');
|
|
45
|
+
const [selectedEndpoint, setSelectedEndpoint] = useState<string | null>(null);
|
|
46
|
+
|
|
47
|
+
// Close on Escape key
|
|
48
|
+
const handleKeyDown = useCallback(
|
|
49
|
+
(e: KeyboardEvent) => {
|
|
50
|
+
if (e.key === 'Escape') {
|
|
51
|
+
onClose();
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
[onClose]
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
59
|
+
document.body.style.overflow = 'hidden';
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
63
|
+
document.body.style.overflow = '';
|
|
64
|
+
};
|
|
65
|
+
}, [handleKeyDown]);
|
|
66
|
+
|
|
67
|
+
// Get endpoints - either from endpoints object or default single endpoint
|
|
68
|
+
const endpoints = data.endpoints || { default: { methods: data.methods || ['POST'] } };
|
|
69
|
+
const endpointKeys = Object.keys(endpoints);
|
|
70
|
+
const hasMultipleEndpoints = endpointKeys.length > 1;
|
|
71
|
+
|
|
72
|
+
// Set initial endpoint
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (endpointKeys.length > 0 && !selectedEndpoint) {
|
|
75
|
+
setSelectedEndpoint(endpointKeys[0]);
|
|
76
|
+
}
|
|
77
|
+
}, [endpointKeys, selectedEndpoint]);
|
|
78
|
+
|
|
79
|
+
const currentEndpoint = selectedEndpoint ? endpoints[selectedEndpoint] : endpoints[endpointKeys[0]];
|
|
80
|
+
const methods = currentEndpoint?.methods || data.methods || ['POST'];
|
|
81
|
+
const baseUrl = typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000';
|
|
82
|
+
|
|
83
|
+
// Build endpoint path
|
|
84
|
+
const getEndpointPath = () => {
|
|
85
|
+
if (selectedEndpoint && selectedEndpoint !== 'default') {
|
|
86
|
+
return `/api/v2/${id}/${selectedEndpoint}`;
|
|
87
|
+
}
|
|
88
|
+
return `/api/v2/${id}`;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const endpoint = getEndpointPath();
|
|
92
|
+
|
|
93
|
+
// Generate curl example
|
|
94
|
+
const generateCurl = (method: string) => {
|
|
95
|
+
if (method === 'GET') {
|
|
96
|
+
return `curl -X GET "${baseUrl}${endpoint}"`;
|
|
97
|
+
}
|
|
98
|
+
return `curl -X ${method} "${baseUrl}${endpoint}" \\
|
|
99
|
+
-H "Content-Type: application/json" \\
|
|
100
|
+
-d '{
|
|
101
|
+
"example": "value"
|
|
102
|
+
}'`;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div
|
|
107
|
+
className="fixed inset-0 z-50 flex items-center justify-center"
|
|
108
|
+
role="dialog"
|
|
109
|
+
aria-modal="true"
|
|
110
|
+
aria-labelledby="modal-title"
|
|
111
|
+
>
|
|
112
|
+
{/* Backdrop */}
|
|
113
|
+
<div
|
|
114
|
+
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
|
|
115
|
+
onClick={onClose}
|
|
116
|
+
aria-hidden="true"
|
|
117
|
+
/>
|
|
118
|
+
|
|
119
|
+
{/* Modal Content */}
|
|
120
|
+
<div className="relative z-10 flex max-h-[90vh] w-full max-w-5xl flex-col overflow-hidden border-2 border-black bg-white shadow-xl dark:border-gray-600 dark:bg-gray-900">
|
|
121
|
+
{/* Header */}
|
|
122
|
+
<header className="flex items-center justify-between border-b-2 border-black px-6 py-4 dark:border-gray-600">
|
|
123
|
+
<div className="flex items-center gap-4">
|
|
124
|
+
<div>
|
|
125
|
+
<div className="flex items-center gap-2">
|
|
126
|
+
<h2 id="modal-title" className="text-xl font-bold text-black dark:text-white">
|
|
127
|
+
{data.name || id}
|
|
128
|
+
</h2>
|
|
129
|
+
{type === 'combined' && (
|
|
130
|
+
<span className="border border-purple-600 bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-800 dark:bg-purple-900 dark:text-purple-200">
|
|
131
|
+
Combined
|
|
132
|
+
</span>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
136
|
+
{endpoint}
|
|
137
|
+
</p>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{/* Methods */}
|
|
142
|
+
<div className="flex items-center gap-4">
|
|
143
|
+
<div className="flex gap-1">
|
|
144
|
+
{methods.map((method) => (
|
|
145
|
+
<span
|
|
146
|
+
key={method}
|
|
147
|
+
className={`px-2 py-1 text-xs font-medium ${
|
|
148
|
+
method === 'GET'
|
|
149
|
+
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
|
150
|
+
: method === 'POST'
|
|
151
|
+
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
|
|
152
|
+
: method === 'DELETE'
|
|
153
|
+
? 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
|
|
154
|
+
: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200'
|
|
155
|
+
}`}
|
|
156
|
+
>
|
|
157
|
+
{method}
|
|
158
|
+
</span>
|
|
159
|
+
))}
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<button
|
|
163
|
+
onClick={onClose}
|
|
164
|
+
className="border-2 border-black p-2 hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800"
|
|
165
|
+
aria-label="Close"
|
|
166
|
+
>
|
|
167
|
+
<svg
|
|
168
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
169
|
+
width="20"
|
|
170
|
+
height="20"
|
|
171
|
+
viewBox="0 0 24 24"
|
|
172
|
+
fill="none"
|
|
173
|
+
stroke="currentColor"
|
|
174
|
+
strokeWidth="2"
|
|
175
|
+
strokeLinecap="round"
|
|
176
|
+
strokeLinejoin="round"
|
|
177
|
+
>
|
|
178
|
+
<path d="M18 6 6 18" />
|
|
179
|
+
<path d="m6 6 12 12" />
|
|
180
|
+
</svg>
|
|
181
|
+
</button>
|
|
182
|
+
</div>
|
|
183
|
+
</header>
|
|
184
|
+
|
|
185
|
+
{/* Endpoint Selector (for multi-endpoint APIs) */}
|
|
186
|
+
{hasMultipleEndpoints && (
|
|
187
|
+
<div className="border-b-2 border-black bg-gray-50 px-6 py-3 dark:border-gray-600 dark:bg-gray-800">
|
|
188
|
+
<label className="mb-2 block text-sm font-bold text-black dark:text-white">
|
|
189
|
+
Select Endpoint
|
|
190
|
+
</label>
|
|
191
|
+
<div className="flex flex-wrap gap-2">
|
|
192
|
+
{endpointKeys.map((key) => (
|
|
193
|
+
<button
|
|
194
|
+
key={key}
|
|
195
|
+
onClick={() => setSelectedEndpoint(key)}
|
|
196
|
+
className={`border-2 px-3 py-1.5 text-sm font-medium transition-colors ${
|
|
197
|
+
selectedEndpoint === key
|
|
198
|
+
? 'border-[#BA0C2F] bg-[#BA0C2F] text-white'
|
|
199
|
+
: 'border-black bg-white text-black hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-700 dark:text-white'
|
|
200
|
+
}`}
|
|
201
|
+
>
|
|
202
|
+
/{key}
|
|
203
|
+
</button>
|
|
204
|
+
))}
|
|
205
|
+
</div>
|
|
206
|
+
{currentEndpoint?.description && (
|
|
207
|
+
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
208
|
+
{currentEndpoint.description}
|
|
209
|
+
</p>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
{/* Tabs */}
|
|
215
|
+
<div className="border-b-2 border-black px-6 dark:border-gray-600">
|
|
216
|
+
<nav className="flex gap-4">
|
|
217
|
+
<button
|
|
218
|
+
onClick={() => setActiveTab('try-it')}
|
|
219
|
+
className={`border-b-2 py-3 text-sm font-bold transition-colors ${
|
|
220
|
+
activeTab === 'try-it'
|
|
221
|
+
? 'border-[#BA0C2F] text-[#BA0C2F]'
|
|
222
|
+
: 'border-transparent text-gray-600 hover:text-black dark:text-gray-400 dark:hover:text-white'
|
|
223
|
+
}`}
|
|
224
|
+
>
|
|
225
|
+
Try It
|
|
226
|
+
</button>
|
|
227
|
+
<button
|
|
228
|
+
onClick={() => setActiveTab('docs')}
|
|
229
|
+
className={`border-b-2 py-3 text-sm font-bold transition-colors ${
|
|
230
|
+
activeTab === 'docs'
|
|
231
|
+
? 'border-[#BA0C2F] text-[#BA0C2F]'
|
|
232
|
+
: 'border-transparent text-gray-600 hover:text-black dark:text-gray-400 dark:hover:text-white'
|
|
233
|
+
}`}
|
|
234
|
+
>
|
|
235
|
+
Documentation
|
|
236
|
+
</button>
|
|
237
|
+
<button
|
|
238
|
+
onClick={() => setActiveTab('curl')}
|
|
239
|
+
className={`border-b-2 py-3 text-sm font-bold transition-colors ${
|
|
240
|
+
activeTab === 'curl'
|
|
241
|
+
? 'border-[#BA0C2F] text-[#BA0C2F]'
|
|
242
|
+
: 'border-transparent text-gray-600 hover:text-black dark:text-gray-400 dark:hover:text-white'
|
|
243
|
+
}`}
|
|
244
|
+
>
|
|
245
|
+
Curl Examples
|
|
246
|
+
</button>
|
|
247
|
+
</nav>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
{/* Content */}
|
|
251
|
+
<div className="flex-1 overflow-auto p-6">
|
|
252
|
+
{activeTab === 'try-it' && (
|
|
253
|
+
<APITester
|
|
254
|
+
id={id}
|
|
255
|
+
endpoint={endpoint}
|
|
256
|
+
methods={methods}
|
|
257
|
+
selectedEndpoint={selectedEndpoint}
|
|
258
|
+
schemaPath={data.schemas}
|
|
259
|
+
/>
|
|
260
|
+
)}
|
|
261
|
+
|
|
262
|
+
{activeTab === 'docs' && (
|
|
263
|
+
<div className="space-y-6">
|
|
264
|
+
{/* Description */}
|
|
265
|
+
<div>
|
|
266
|
+
<h3 className="mb-2 text-lg font-bold text-black dark:text-white">Description</h3>
|
|
267
|
+
<p className="text-gray-600 dark:text-gray-400">
|
|
268
|
+
{data.description || `API endpoint for ${id}`}
|
|
269
|
+
</p>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
{/* File Locations */}
|
|
273
|
+
<div>
|
|
274
|
+
<h3 className="mb-2 text-lg font-bold text-black dark:text-white">File Locations</h3>
|
|
275
|
+
<div className="space-y-2 border-2 border-black bg-gray-50 p-4 dark:border-gray-600 dark:bg-gray-800">
|
|
276
|
+
<div className="flex justify-between">
|
|
277
|
+
<span className="text-sm text-gray-600 dark:text-gray-400">Route:</span>
|
|
278
|
+
<code className="text-sm text-black dark:text-white">{data.route || `src/app/api/v2/${id}/route.ts`}</code>
|
|
279
|
+
</div>
|
|
280
|
+
{data.schemas && (
|
|
281
|
+
<div className="flex justify-between">
|
|
282
|
+
<span className="text-sm text-gray-600 dark:text-gray-400">Schemas:</span>
|
|
283
|
+
<code className="text-sm text-black dark:text-white">{data.schemas}</code>
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
{data.tests && (
|
|
287
|
+
<div className="flex justify-between">
|
|
288
|
+
<span className="text-sm text-gray-600 dark:text-gray-400">Tests:</span>
|
|
289
|
+
<code className="text-sm text-black dark:text-white">{data.tests}</code>
|
|
290
|
+
</div>
|
|
291
|
+
)}
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
{/* Combined Info */}
|
|
296
|
+
{type === 'combined' && data.combines && (
|
|
297
|
+
<div>
|
|
298
|
+
<h3 className="mb-2 text-lg font-bold text-black dark:text-white">Combined APIs</h3>
|
|
299
|
+
<div className="border-2 border-black bg-gray-50 p-4 dark:border-gray-600 dark:bg-gray-800">
|
|
300
|
+
<p className="mb-2 text-sm text-gray-600 dark:text-gray-400">
|
|
301
|
+
This endpoint orchestrates the following APIs:
|
|
302
|
+
</p>
|
|
303
|
+
<ul className="list-inside list-disc space-y-1">
|
|
304
|
+
{data.combines.map((api) => (
|
|
305
|
+
<li key={api} className="text-sm text-black dark:text-white">
|
|
306
|
+
{api}
|
|
307
|
+
</li>
|
|
308
|
+
))}
|
|
309
|
+
</ul>
|
|
310
|
+
{data.flow_type && (
|
|
311
|
+
<p className="mt-3 text-sm">
|
|
312
|
+
<span className="text-gray-600 dark:text-gray-400">Flow type:</span>{' '}
|
|
313
|
+
<span className="font-bold text-black dark:text-white">{data.flow_type}</span>
|
|
314
|
+
</p>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
)}
|
|
319
|
+
</div>
|
|
320
|
+
)}
|
|
321
|
+
|
|
322
|
+
{activeTab === 'curl' && (
|
|
323
|
+
<div className="space-y-4">
|
|
324
|
+
{methods.map((method) => (
|
|
325
|
+
<div key={method}>
|
|
326
|
+
<h3 className="mb-2 text-sm font-bold text-black dark:text-white">{method} Request</h3>
|
|
327
|
+
<div className="relative">
|
|
328
|
+
<pre className="overflow-x-auto border-2 border-black bg-zinc-900 p-4 text-sm text-zinc-100">
|
|
329
|
+
<code>{generateCurl(method)}</code>
|
|
330
|
+
</pre>
|
|
331
|
+
<button
|
|
332
|
+
onClick={() => navigator.clipboard.writeText(generateCurl(method))}
|
|
333
|
+
className="absolute right-2 top-2 border border-zinc-600 bg-zinc-700 px-2 py-1 text-xs text-zinc-300 hover:bg-zinc-600"
|
|
334
|
+
>
|
|
335
|
+
Copy
|
|
336
|
+
</button>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
))}
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
{/* Footer */}
|
|
345
|
+
<footer className="border-t-2 border-black px-6 py-4 dark:border-gray-600">
|
|
346
|
+
<div className="flex items-center justify-between">
|
|
347
|
+
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
348
|
+
{data.created_at && `Created: ${data.created_at}`}
|
|
349
|
+
</p>
|
|
350
|
+
<div className="flex gap-2">
|
|
351
|
+
{data.tests && (
|
|
352
|
+
<button className="border-2 border-black px-3 py-1.5 text-sm font-medium hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800">
|
|
353
|
+
View Tests
|
|
354
|
+
</button>
|
|
355
|
+
)}
|
|
356
|
+
<button
|
|
357
|
+
onClick={() => {
|
|
358
|
+
const importPath = data.schemas?.replace(/^src\//, '@/').replace(/\.ts$/, '');
|
|
359
|
+
if (importPath) {
|
|
360
|
+
navigator.clipboard.writeText(
|
|
361
|
+
`import { RequestSchema, ResponseSchema } from '${importPath}';`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}}
|
|
365
|
+
className="border-2 border-black bg-[#BA0C2F] px-3 py-1.5 text-sm font-medium text-white hover:bg-[#8a0923]"
|
|
366
|
+
>
|
|
367
|
+
Copy Schema Import
|
|
368
|
+
</button>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
</footer>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
);
|
|
375
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from 'react';
|
|
4
|
+
import { APICard } from './APICard';
|
|
5
|
+
import { APIModal } from './APIModal';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Registry structure from .claude/registry.json
|
|
9
|
+
*/
|
|
10
|
+
interface RegistryAPI {
|
|
11
|
+
name: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
route: string;
|
|
14
|
+
schemas?: string;
|
|
15
|
+
tests?: string;
|
|
16
|
+
methods?: string[];
|
|
17
|
+
created_at?: string;
|
|
18
|
+
status?: string;
|
|
19
|
+
endpoints?: Record<string, {
|
|
20
|
+
methods: string[];
|
|
21
|
+
description?: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface Registry {
|
|
26
|
+
version: string;
|
|
27
|
+
apis: Record<string, RegistryAPI>;
|
|
28
|
+
combined?: Record<string, RegistryAPI & { combines?: string[]; flow_type?: string }>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface APIShowcaseProps {
|
|
32
|
+
registry?: Registry;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* API Showcase Component
|
|
37
|
+
*
|
|
38
|
+
* Displays a grid of all registered APIs with filtering and search.
|
|
39
|
+
* Click any card to open the interactive testing modal.
|
|
40
|
+
*
|
|
41
|
+
* Data source: .claude/registry.json (apis + combined sections)
|
|
42
|
+
*
|
|
43
|
+
* Created with Hustle API Dev Tools (v3.9.2)
|
|
44
|
+
*/
|
|
45
|
+
export function APIShowcase({ registry: propRegistry }: APIShowcaseProps) {
|
|
46
|
+
const [selectedAPI, setSelectedAPI] = useState<{
|
|
47
|
+
id: string;
|
|
48
|
+
type: 'api' | 'combined';
|
|
49
|
+
data: RegistryAPI;
|
|
50
|
+
} | null>(null);
|
|
51
|
+
const [filter, setFilter] = useState<'all' | 'api' | 'combined'>('all');
|
|
52
|
+
const [search, setSearch] = useState('');
|
|
53
|
+
|
|
54
|
+
// Use prop registry or default empty structure
|
|
55
|
+
const registry: Registry = propRegistry || {
|
|
56
|
+
version: '1.0.0',
|
|
57
|
+
apis: {},
|
|
58
|
+
combined: {},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Combine APIs and combined endpoints into single list
|
|
62
|
+
const allAPIs = useMemo(() => {
|
|
63
|
+
const items: Array<{ id: string; type: 'api' | 'combined'; data: RegistryAPI }> = [];
|
|
64
|
+
|
|
65
|
+
// Add regular APIs
|
|
66
|
+
Object.entries(registry.apis || {}).forEach(([id, data]) => {
|
|
67
|
+
items.push({ id, type: 'api', data });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Add combined APIs
|
|
71
|
+
Object.entries(registry.combined || {}).forEach(([id, data]) => {
|
|
72
|
+
items.push({ id, type: 'combined', data });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return items;
|
|
76
|
+
}, [registry]);
|
|
77
|
+
|
|
78
|
+
// Filter and search
|
|
79
|
+
const filteredAPIs = useMemo(() => {
|
|
80
|
+
return allAPIs.filter((item) => {
|
|
81
|
+
// Type filter
|
|
82
|
+
if (filter !== 'all' && item.type !== filter) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Search filter
|
|
87
|
+
if (search) {
|
|
88
|
+
const searchLower = search.toLowerCase();
|
|
89
|
+
return (
|
|
90
|
+
item.id.toLowerCase().includes(searchLower) ||
|
|
91
|
+
item.data.name?.toLowerCase().includes(searchLower) ||
|
|
92
|
+
item.data.description?.toLowerCase().includes(searchLower)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return true;
|
|
97
|
+
});
|
|
98
|
+
}, [allAPIs, filter, search]);
|
|
99
|
+
|
|
100
|
+
// Stats
|
|
101
|
+
const stats = useMemo(() => {
|
|
102
|
+
return {
|
|
103
|
+
total: allAPIs.length,
|
|
104
|
+
apis: Object.keys(registry.apis || {}).length,
|
|
105
|
+
combined: Object.keys(registry.combined || {}).length,
|
|
106
|
+
};
|
|
107
|
+
}, [allAPIs, registry]);
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div>
|
|
111
|
+
{/* Stats Bar */}
|
|
112
|
+
<div className="mb-6 flex flex-wrap items-center gap-4 border-2 border-black bg-gray-50 p-4 dark:border-gray-600 dark:bg-gray-800">
|
|
113
|
+
<div className="flex items-center gap-2">
|
|
114
|
+
<span className="text-2xl font-bold text-black dark:text-white">{stats.total}</span>
|
|
115
|
+
<span className="text-gray-600 dark:text-gray-400">Total APIs</span>
|
|
116
|
+
</div>
|
|
117
|
+
<div className="h-8 w-px bg-black dark:bg-gray-600" />
|
|
118
|
+
<div className="flex items-center gap-2">
|
|
119
|
+
<span className="font-bold text-black dark:text-white">{stats.apis}</span>
|
|
120
|
+
<span className="text-gray-600 dark:text-gray-400">Endpoints</span>
|
|
121
|
+
</div>
|
|
122
|
+
<div className="flex items-center gap-2">
|
|
123
|
+
<span className="font-bold text-black dark:text-white">{stats.combined}</span>
|
|
124
|
+
<span className="text-gray-600 dark:text-gray-400">Combined</span>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Filters */}
|
|
129
|
+
<div className="mb-6 flex flex-wrap items-center gap-4">
|
|
130
|
+
{/* Search */}
|
|
131
|
+
<div className="flex-1">
|
|
132
|
+
<input
|
|
133
|
+
type="text"
|
|
134
|
+
placeholder="Search APIs..."
|
|
135
|
+
value={search}
|
|
136
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
137
|
+
className="w-full max-w-md border-2 border-black bg-white px-4 py-2 focus:border-[#BA0C2F] focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{/* Type Filter */}
|
|
142
|
+
<div className="flex gap-2">
|
|
143
|
+
<button
|
|
144
|
+
onClick={() => setFilter('all')}
|
|
145
|
+
className={`border-2 px-4 py-2 text-sm font-bold transition-colors ${
|
|
146
|
+
filter === 'all'
|
|
147
|
+
? 'border-[#BA0C2F] bg-[#BA0C2F] text-white'
|
|
148
|
+
: 'border-black bg-white text-black hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-800 dark:text-white'
|
|
149
|
+
}`}
|
|
150
|
+
>
|
|
151
|
+
All ({stats.total})
|
|
152
|
+
</button>
|
|
153
|
+
<button
|
|
154
|
+
onClick={() => setFilter('api')}
|
|
155
|
+
className={`border-2 px-4 py-2 text-sm font-bold transition-colors ${
|
|
156
|
+
filter === 'api'
|
|
157
|
+
? 'border-[#BA0C2F] bg-[#BA0C2F] text-white'
|
|
158
|
+
: 'border-black bg-white text-black hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-800 dark:text-white'
|
|
159
|
+
}`}
|
|
160
|
+
>
|
|
161
|
+
APIs ({stats.apis})
|
|
162
|
+
</button>
|
|
163
|
+
<button
|
|
164
|
+
onClick={() => setFilter('combined')}
|
|
165
|
+
className={`border-2 px-4 py-2 text-sm font-bold transition-colors ${
|
|
166
|
+
filter === 'combined'
|
|
167
|
+
? 'border-[#BA0C2F] bg-[#BA0C2F] text-white'
|
|
168
|
+
: 'border-black bg-white text-black hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-800 dark:text-white'
|
|
169
|
+
}`}
|
|
170
|
+
>
|
|
171
|
+
Combined ({stats.combined})
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{/* Grid */}
|
|
177
|
+
{filteredAPIs.length > 0 ? (
|
|
178
|
+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
179
|
+
{filteredAPIs.map((item) => (
|
|
180
|
+
<APICard
|
|
181
|
+
key={`${item.type}-${item.id}`}
|
|
182
|
+
id={item.id}
|
|
183
|
+
type={item.type}
|
|
184
|
+
data={item.data}
|
|
185
|
+
onClick={() => setSelectedAPI(item)}
|
|
186
|
+
/>
|
|
187
|
+
))}
|
|
188
|
+
</div>
|
|
189
|
+
) : (
|
|
190
|
+
<div className="flex flex-col items-center justify-center border-2 border-dashed border-black py-16 dark:border-gray-600">
|
|
191
|
+
<div className="mb-4 border-2 border-black bg-gray-100 p-4 dark:border-gray-600 dark:bg-gray-800">
|
|
192
|
+
<svg
|
|
193
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
194
|
+
width="32"
|
|
195
|
+
height="32"
|
|
196
|
+
viewBox="0 0 24 24"
|
|
197
|
+
fill="none"
|
|
198
|
+
stroke="currentColor"
|
|
199
|
+
strokeWidth="2"
|
|
200
|
+
strokeLinecap="round"
|
|
201
|
+
strokeLinejoin="round"
|
|
202
|
+
className="text-gray-500"
|
|
203
|
+
>
|
|
204
|
+
<path d="M10 10h.01" />
|
|
205
|
+
<path d="M14 10h.01" />
|
|
206
|
+
<path d="M12 14a4 4 0 0 0 0-8" />
|
|
207
|
+
<path d="M16 10a4 4 0 0 0-8 0" />
|
|
208
|
+
<circle cx="12" cy="12" r="10" />
|
|
209
|
+
</svg>
|
|
210
|
+
</div>
|
|
211
|
+
<p className="text-lg font-bold text-black dark:text-white">No APIs found</p>
|
|
212
|
+
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
213
|
+
{search
|
|
214
|
+
? 'Try a different search term'
|
|
215
|
+
: 'Create your first API with /api-create'}
|
|
216
|
+
</p>
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{/* Modal */}
|
|
221
|
+
{selectedAPI && (
|
|
222
|
+
<APIModal
|
|
223
|
+
id={selectedAPI.id}
|
|
224
|
+
type={selectedAPI.type}
|
|
225
|
+
data={selectedAPI.data}
|
|
226
|
+
onClose={() => setSelectedAPI(null)}
|
|
227
|
+
/>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|