@hustle-together/api-dev-tools 3.6.4 → 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.
Files changed (61) hide show
  1. package/README.md +5307 -258
  2. package/bin/cli.js +348 -20
  3. package/commands/README.md +459 -71
  4. package/commands/hustle-api-continue.md +158 -0
  5. package/commands/{api-create.md → hustle-api-create.md} +22 -2
  6. package/commands/{api-env.md → hustle-api-env.md} +4 -4
  7. package/commands/{api-interview.md → hustle-api-interview.md} +1 -1
  8. package/commands/{api-research.md → hustle-api-research.md} +3 -3
  9. package/commands/hustle-api-sessions.md +149 -0
  10. package/commands/{api-status.md → hustle-api-status.md} +16 -16
  11. package/commands/{api-verify.md → hustle-api-verify.md} +2 -2
  12. package/commands/hustle-combine.md +763 -0
  13. package/commands/hustle-ui-create.md +825 -0
  14. package/hooks/api-workflow-check.py +385 -19
  15. package/hooks/cache-research.py +337 -0
  16. package/hooks/check-playwright-setup.py +103 -0
  17. package/hooks/check-storybook-setup.py +81 -0
  18. package/hooks/detect-interruption.py +165 -0
  19. package/hooks/enforce-brand-guide.py +131 -0
  20. package/hooks/enforce-documentation.py +60 -8
  21. package/hooks/enforce-freshness.py +184 -0
  22. package/hooks/enforce-questions-sourced.py +146 -0
  23. package/hooks/enforce-schema-from-interview.py +248 -0
  24. package/hooks/enforce-ui-disambiguation.py +108 -0
  25. package/hooks/enforce-ui-interview.py +130 -0
  26. package/hooks/generate-manifest-entry.py +981 -0
  27. package/hooks/session-logger.py +297 -0
  28. package/hooks/session-startup.py +65 -10
  29. package/hooks/track-scope-coverage.py +220 -0
  30. package/hooks/track-tool-use.py +81 -1
  31. package/hooks/update-api-showcase.py +149 -0
  32. package/hooks/update-registry.py +352 -0
  33. package/hooks/update-ui-showcase.py +148 -0
  34. package/package.json +8 -2
  35. package/templates/BRAND_GUIDE.md +299 -0
  36. package/templates/CLAUDE-SECTION.md +56 -24
  37. package/templates/SPEC.json +640 -0
  38. package/templates/api-dev-state.json +179 -161
  39. package/templates/api-showcase/APICard.tsx +153 -0
  40. package/templates/api-showcase/APIModal.tsx +375 -0
  41. package/templates/api-showcase/APIShowcase.tsx +231 -0
  42. package/templates/api-showcase/APITester.tsx +522 -0
  43. package/templates/api-showcase/page.tsx +41 -0
  44. package/templates/component/Component.stories.tsx +172 -0
  45. package/templates/component/Component.test.tsx +237 -0
  46. package/templates/component/Component.tsx +86 -0
  47. package/templates/component/Component.types.ts +55 -0
  48. package/templates/component/index.ts +15 -0
  49. package/templates/dev-tools/_components/DevToolsLanding.tsx +320 -0
  50. package/templates/dev-tools/page.tsx +10 -0
  51. package/templates/page/page.e2e.test.ts +218 -0
  52. package/templates/page/page.tsx +42 -0
  53. package/templates/performance-budgets.json +58 -0
  54. package/templates/registry.json +13 -0
  55. package/templates/settings.json +74 -0
  56. package/templates/shared/HeroHeader.tsx +261 -0
  57. package/templates/shared/index.ts +1 -0
  58. package/templates/ui-showcase/PreviewCard.tsx +315 -0
  59. package/templates/ui-showcase/PreviewModal.tsx +676 -0
  60. package/templates/ui-showcase/UIShowcase.tsx +262 -0
  61. 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
+ }