@djangocfg/ui-nextjs 1.4.45
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/LICENSE +21 -0
- package/README.md +152 -0
- package/package.json +110 -0
- package/src/animations/AnimatedBackground.tsx +645 -0
- package/src/animations/index.ts +2 -0
- package/src/blocks/ArticleCard.tsx +94 -0
- package/src/blocks/ArticleList.tsx +95 -0
- package/src/blocks/CTASection.tsx +136 -0
- package/src/blocks/FeatureSection.tsx +104 -0
- package/src/blocks/Hero.tsx +102 -0
- package/src/blocks/NewsletterSection.tsx +119 -0
- package/src/blocks/StatsSection.tsx +103 -0
- package/src/blocks/SuperHero.tsx +328 -0
- package/src/blocks/TestimonialSection.tsx +122 -0
- package/src/blocks/index.ts +9 -0
- package/src/components/README.md +2018 -0
- package/src/components/breadcrumb-navigation.tsx +127 -0
- package/src/components/breadcrumb.tsx +132 -0
- package/src/components/button-download.tsx +275 -0
- package/src/components/dropdown-menu.tsx +219 -0
- package/src/components/index.ts +86 -0
- package/src/components/markdown/MarkdownMessage.tsx +338 -0
- package/src/components/markdown/index.ts +5 -0
- package/src/components/menubar.tsx +274 -0
- package/src/components/multi-select-pro/async.tsx +608 -0
- package/src/components/multi-select-pro/helpers.tsx +84 -0
- package/src/components/multi-select-pro/index.tsx +622 -0
- package/src/components/navigation-menu.tsx +153 -0
- package/src/components/pagination-static.tsx +348 -0
- package/src/components/pagination.tsx +138 -0
- package/src/components/phone-input.tsx +276 -0
- package/src/components/sidebar.tsx +866 -0
- package/src/components/sonner.tsx +31 -0
- package/src/components/ssr-pagination.tsx +237 -0
- package/src/hooks/index.ts +19 -0
- package/src/hooks/useCfgRouter.ts +153 -0
- package/src/hooks/useLocalStorage.ts +221 -0
- package/src/hooks/useQueryParams.ts +73 -0
- package/src/hooks/useSessionStorage.ts +188 -0
- package/src/hooks/useTheme.ts +57 -0
- package/src/index.ts +24 -0
- package/src/lib/index.ts +2 -0
- package/src/styles/index.css +2 -0
- package/src/theme/ForceTheme.tsx +115 -0
- package/src/theme/ThemeProvider.tsx +82 -0
- package/src/theme/ThemeToggle.tsx +52 -0
- package/src/theme/index.ts +3 -0
- package/src/tools/JsonForm/JsonSchemaForm.tsx +199 -0
- package/src/tools/JsonForm/examples/BotConfigExample.tsx +245 -0
- package/src/tools/JsonForm/examples/RealBotConfigExample.tsx +157 -0
- package/src/tools/JsonForm/index.ts +46 -0
- package/src/tools/JsonForm/templates/ArrayFieldItemTemplate.tsx +46 -0
- package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +73 -0
- package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +106 -0
- package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +34 -0
- package/src/tools/JsonForm/templates/FieldTemplate.tsx +61 -0
- package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +43 -0
- package/src/tools/JsonForm/templates/index.ts +12 -0
- package/src/tools/JsonForm/types.ts +83 -0
- package/src/tools/JsonForm/utils.ts +212 -0
- package/src/tools/JsonForm/widgets/CheckboxWidget.tsx +36 -0
- package/src/tools/JsonForm/widgets/NumberWidget.tsx +88 -0
- package/src/tools/JsonForm/widgets/SelectWidget.tsx +100 -0
- package/src/tools/JsonForm/widgets/SwitchWidget.tsx +34 -0
- package/src/tools/JsonForm/widgets/TextWidget.tsx +95 -0
- package/src/tools/JsonForm/widgets/index.ts +12 -0
- package/src/tools/JsonTree/index.tsx +252 -0
- package/src/tools/LottiePlayer/LottiePlayer.client.tsx +212 -0
- package/src/tools/LottiePlayer/index.tsx +54 -0
- package/src/tools/LottiePlayer/types.ts +108 -0
- package/src/tools/LottiePlayer/useLottie.ts +163 -0
- package/src/tools/Mermaid/Mermaid.client.tsx +341 -0
- package/src/tools/Mermaid/index.tsx +40 -0
- package/src/tools/OpenapiViewer/components/EndpointInfo.tsx +144 -0
- package/src/tools/OpenapiViewer/components/EndpointsLibrary.tsx +255 -0
- package/src/tools/OpenapiViewer/components/PlaygroundLayout.tsx +123 -0
- package/src/tools/OpenapiViewer/components/PlaygroundStepper.tsx +98 -0
- package/src/tools/OpenapiViewer/components/RequestBuilder.tsx +164 -0
- package/src/tools/OpenapiViewer/components/RequestParametersForm.tsx +253 -0
- package/src/tools/OpenapiViewer/components/ResponseViewer.tsx +169 -0
- package/src/tools/OpenapiViewer/components/VersionSelector.tsx +64 -0
- package/src/tools/OpenapiViewer/components/index.ts +14 -0
- package/src/tools/OpenapiViewer/constants.ts +39 -0
- package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +338 -0
- package/src/tools/OpenapiViewer/hooks/index.ts +8 -0
- package/src/tools/OpenapiViewer/hooks/useMobile.ts +10 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +203 -0
- package/src/tools/OpenapiViewer/index.tsx +36 -0
- package/src/tools/OpenapiViewer/types.ts +152 -0
- package/src/tools/OpenapiViewer/utils/apiKeyManager.ts +149 -0
- package/src/tools/OpenapiViewer/utils/formatters.ts +71 -0
- package/src/tools/OpenapiViewer/utils/index.ts +9 -0
- package/src/tools/OpenapiViewer/utils/versionManager.ts +161 -0
- package/src/tools/PrettyCode/PrettyCode.client.tsx +217 -0
- package/src/tools/PrettyCode/index.tsx +43 -0
- package/src/tools/VideoPlayer/README.md +239 -0
- package/src/tools/VideoPlayer/VideoControls.tsx +138 -0
- package/src/tools/VideoPlayer/VideoPlayer.tsx +230 -0
- package/src/tools/VideoPlayer/index.ts +9 -0
- package/src/tools/VideoPlayer/types.ts +62 -0
- package/src/tools/index.ts +43 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { createContext, ReactNode, useCallback, useContext, useEffect, useState } from 'react';
|
|
4
|
+
import consola from 'consola';
|
|
5
|
+
import { type ApiEndpoint, type ApiResponse, type PlaygroundContextType, type PlaygroundState, type PlaygroundStep, type PlaygroundConfig } from '../types';
|
|
6
|
+
import { getDefaultVersion } from '../utils/versionManager';
|
|
7
|
+
import { parseRequestHeaders, substituteUrlParameters } from '../utils';
|
|
8
|
+
import { useCopy } from '@djangocfg/ui-core/hooks';
|
|
9
|
+
|
|
10
|
+
const createInitialState = (): PlaygroundState => ({
|
|
11
|
+
// Step management
|
|
12
|
+
currentStep: 'endpoints',
|
|
13
|
+
steps: ['endpoints', 'request', 'response'],
|
|
14
|
+
|
|
15
|
+
// Endpoint selection
|
|
16
|
+
selectedEndpoint: null,
|
|
17
|
+
selectedCategory: 'All',
|
|
18
|
+
searchTerm: '',
|
|
19
|
+
selectedVersion: getDefaultVersion().id,
|
|
20
|
+
|
|
21
|
+
// Request configuration
|
|
22
|
+
requestUrl: '',
|
|
23
|
+
requestMethod: 'GET',
|
|
24
|
+
requestHeaders: '{\n "Content-Type": "application/json"\n}',
|
|
25
|
+
requestBody: '',
|
|
26
|
+
selectedApiKey: null,
|
|
27
|
+
manualApiToken: '',
|
|
28
|
+
parameters: {},
|
|
29
|
+
|
|
30
|
+
// Response
|
|
31
|
+
response: null,
|
|
32
|
+
loading: false,
|
|
33
|
+
|
|
34
|
+
// UI state
|
|
35
|
+
sidebarOpen: false,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const PlaygroundContext = createContext<PlaygroundContextType | undefined>(undefined);
|
|
39
|
+
|
|
40
|
+
export const usePlaygroundContext = () => {
|
|
41
|
+
const context = useContext(PlaygroundContext);
|
|
42
|
+
if (!context) {
|
|
43
|
+
throw new Error('usePlaygroundContext must be used within a PlaygroundProvider');
|
|
44
|
+
}
|
|
45
|
+
return context;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
interface PlaygroundProviderProps {
|
|
49
|
+
children: ReactNode;
|
|
50
|
+
config: PlaygroundConfig;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const PlaygroundProvider: React.FC<PlaygroundProviderProps> = ({ children, config }) => {
|
|
54
|
+
const [state, setState] = useState<PlaygroundState>(() => createInitialState());
|
|
55
|
+
|
|
56
|
+
// TODO: Get API keys from CFG context - temporarily disabled
|
|
57
|
+
// const { apiKeys: apiKeysResponse, isLoadingApiKeys } = useApiKeysContext();
|
|
58
|
+
// const apiKeys = (apiKeysResponse && apiKeysResponse.results) ? apiKeysResponse.results : [];
|
|
59
|
+
const apiKeys = React.useMemo(() => [], []);
|
|
60
|
+
const isLoadingApiKeys = false;
|
|
61
|
+
|
|
62
|
+
const { copyToClipboard } = useCopy({
|
|
63
|
+
successMessage: "cURL command copied to clipboard",
|
|
64
|
+
errorMessage: "Failed to copy cURL command"
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const updateState = (updates: Partial<PlaygroundState>) => {
|
|
68
|
+
setState((prev) => ({ ...prev, ...updates }));
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Auto-select first API key when available
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!isLoadingApiKeys && apiKeys.length > 0 && !state.selectedApiKey) {
|
|
74
|
+
updateState({ selectedApiKey: apiKeys[0]?.id || null });
|
|
75
|
+
}
|
|
76
|
+
}, [apiKeys, isLoadingApiKeys, state.selectedApiKey]);
|
|
77
|
+
|
|
78
|
+
// Update headers when API key changes
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
try {
|
|
81
|
+
setState(prev => {
|
|
82
|
+
const headers = parseRequestHeaders(prev.requestHeaders);
|
|
83
|
+
let hasChanged = false;
|
|
84
|
+
|
|
85
|
+
if (prev.selectedApiKey) {
|
|
86
|
+
const apiKey = apiKeys.find(k => k.id === prev.selectedApiKey);
|
|
87
|
+
|
|
88
|
+
if (apiKey) {
|
|
89
|
+
// Add API key to headers only if it changed
|
|
90
|
+
if (headers['X-API-Key'] !== apiKey.id) {
|
|
91
|
+
headers['X-API-Key'] = apiKey.id;
|
|
92
|
+
hasChanged = true;
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
// Selected API key no longer exists, clear selection
|
|
96
|
+
return { ...prev, selectedApiKey: null };
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
// Remove API key from headers if no key is selected
|
|
100
|
+
if (headers['X-API-Key']) {
|
|
101
|
+
delete headers['X-API-Key'];
|
|
102
|
+
hasChanged = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Only update if headers actually changed
|
|
107
|
+
if (hasChanged) {
|
|
108
|
+
const updatedHeaders = JSON.stringify(headers, null, 2);
|
|
109
|
+
return { ...prev, requestHeaders: updatedHeaders };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return prev;
|
|
113
|
+
});
|
|
114
|
+
} catch (error) {
|
|
115
|
+
consola.error('Error updating headers:', error);
|
|
116
|
+
}
|
|
117
|
+
}, [state.selectedApiKey, apiKeys]); // Removed state.requestHeaders dependency
|
|
118
|
+
|
|
119
|
+
// Update URL when parameters change
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (state.selectedEndpoint && state.parameters) {
|
|
122
|
+
// Path is already a full URL from the endpoint
|
|
123
|
+
const updatedUrl = substituteUrlParameters(state.selectedEndpoint.path, state.parameters);
|
|
124
|
+
|
|
125
|
+
// Only update if URL actually changed to avoid infinite loop
|
|
126
|
+
if (updatedUrl !== state.requestUrl) {
|
|
127
|
+
updateState({ requestUrl: updatedUrl });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}, [state.parameters, state.selectedEndpoint, state.requestUrl]);
|
|
131
|
+
|
|
132
|
+
// Step management
|
|
133
|
+
const setCurrentStep = (step: PlaygroundStep) => {
|
|
134
|
+
updateState({ currentStep: step });
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const goToNextStep = () => {
|
|
138
|
+
const currentIndex = state.steps.indexOf(state.currentStep);
|
|
139
|
+
if (currentIndex < state.steps.length - 1) {
|
|
140
|
+
updateState({ currentStep: state.steps[currentIndex + 1] });
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const goToPreviousStep = () => {
|
|
145
|
+
const currentIndex = state.steps.indexOf(state.currentStep);
|
|
146
|
+
if (currentIndex > 0) {
|
|
147
|
+
updateState({ currentStep: state.steps[currentIndex - 1] });
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Endpoint management
|
|
152
|
+
const setSelectedEndpoint = (endpoint: ApiEndpoint | null) => {
|
|
153
|
+
if (endpoint) {
|
|
154
|
+
// All endpoints are GET only
|
|
155
|
+
// Path is already a full URL from the endpoint
|
|
156
|
+
updateState({
|
|
157
|
+
selectedEndpoint: endpoint,
|
|
158
|
+
requestMethod: 'GET',
|
|
159
|
+
requestUrl: endpoint.path,
|
|
160
|
+
parameters: {}, // Reset parameters when endpoint changes
|
|
161
|
+
currentStep: 'request'
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
updateState({ selectedEndpoint: endpoint });
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const setSelectedCategory = (category: string) => {
|
|
169
|
+
updateState({ selectedCategory: category });
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const setSearchTerm = (term: string) => {
|
|
173
|
+
updateState({ searchTerm: term });
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const setSelectedVersion = (version: string) => {
|
|
177
|
+
updateState({ selectedVersion: version });
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Request management
|
|
181
|
+
const setRequestUrl = (url: string) => {
|
|
182
|
+
updateState({ requestUrl: url });
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const setRequestMethod = (method: string) => {
|
|
186
|
+
updateState({ requestMethod: method });
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const setRequestHeaders = (headers: string) => {
|
|
190
|
+
updateState({ requestHeaders: headers });
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const setRequestBody = (body: string) => {
|
|
194
|
+
updateState({ requestBody: body });
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const setSelectedApiKey = (apiKeyId: string | null) => {
|
|
198
|
+
updateState({ selectedApiKey: apiKeyId });
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const setManualApiToken = (manualApiToken: string) => {
|
|
202
|
+
updateState({ manualApiToken });
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const setParameters = (parameters: Record<string, string>) => {
|
|
206
|
+
updateState({ parameters });
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Response management
|
|
210
|
+
const setResponse = (response: ApiResponse | null) => {
|
|
211
|
+
updateState({ response });
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const setLoading = (loading: boolean) => {
|
|
215
|
+
updateState({ loading });
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// UI management
|
|
219
|
+
const setSidebarOpen = (sidebarOpen: boolean) => {
|
|
220
|
+
updateState({ sidebarOpen });
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// Actions
|
|
224
|
+
const clearAll = useCallback(() => {
|
|
225
|
+
setState(createInitialState());
|
|
226
|
+
}, []);
|
|
227
|
+
|
|
228
|
+
const sendRequest = useCallback(async () => {
|
|
229
|
+
if (!state.requestUrl) {
|
|
230
|
+
consola.error('No URL provided');
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
setLoading(true);
|
|
235
|
+
setResponse(null);
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const headers = parseRequestHeaders(state.requestHeaders);
|
|
239
|
+
|
|
240
|
+
// Bearer token priority: manual token → JWT token from localStorage
|
|
241
|
+
let bearerToken: string | null = null;
|
|
242
|
+
|
|
243
|
+
if (state.manualApiToken) {
|
|
244
|
+
// Use manual token if provided
|
|
245
|
+
bearerToken = state.manualApiToken;
|
|
246
|
+
} else {
|
|
247
|
+
// Try to get JWT token from localStorage
|
|
248
|
+
if (typeof window !== 'undefined') {
|
|
249
|
+
bearerToken = window.localStorage.getItem('auth_token');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (bearerToken) {
|
|
254
|
+
headers['Authorization'] = `Bearer ${bearerToken}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const requestOptions: RequestInit = {
|
|
258
|
+
method: state.requestMethod,
|
|
259
|
+
headers,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
if (state.requestBody && state.requestMethod !== 'GET') {
|
|
263
|
+
requestOptions.body = state.requestBody;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const response = await fetch(state.requestUrl, requestOptions);
|
|
267
|
+
const responseText = await response.text();
|
|
268
|
+
|
|
269
|
+
let responseData;
|
|
270
|
+
try {
|
|
271
|
+
responseData = JSON.parse(responseText);
|
|
272
|
+
} catch {
|
|
273
|
+
responseData = responseText;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
setResponse({
|
|
277
|
+
status: response.status,
|
|
278
|
+
statusText: response.statusText,
|
|
279
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
280
|
+
data: responseData,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
consola.success(`Request successful: ${state.requestMethod} ${state.requestUrl}`);
|
|
284
|
+
|
|
285
|
+
// Auto-advance to response step
|
|
286
|
+
updateState({ currentStep: 'response' });
|
|
287
|
+
} catch (error) {
|
|
288
|
+
consola.error('Request failed:', error);
|
|
289
|
+
setResponse({
|
|
290
|
+
error: error instanceof Error ? error.message : 'Request failed',
|
|
291
|
+
});
|
|
292
|
+
} finally {
|
|
293
|
+
setLoading(false);
|
|
294
|
+
}
|
|
295
|
+
}, [state, setLoading, setResponse]);
|
|
296
|
+
|
|
297
|
+
const contextValue: PlaygroundContextType = {
|
|
298
|
+
// State
|
|
299
|
+
state,
|
|
300
|
+
config,
|
|
301
|
+
apiKeys,
|
|
302
|
+
apiKeysLoading: isLoadingApiKeys,
|
|
303
|
+
|
|
304
|
+
// Step management
|
|
305
|
+
setCurrentStep,
|
|
306
|
+
goToNextStep,
|
|
307
|
+
goToPreviousStep,
|
|
308
|
+
|
|
309
|
+
// Endpoint management
|
|
310
|
+
setSelectedEndpoint,
|
|
311
|
+
setSelectedCategory,
|
|
312
|
+
setSearchTerm,
|
|
313
|
+
setSelectedVersion,
|
|
314
|
+
|
|
315
|
+
// Request management
|
|
316
|
+
setRequestUrl,
|
|
317
|
+
setRequestMethod,
|
|
318
|
+
setRequestHeaders,
|
|
319
|
+
setRequestBody,
|
|
320
|
+
setSelectedApiKey,
|
|
321
|
+
setManualApiToken,
|
|
322
|
+
setParameters,
|
|
323
|
+
|
|
324
|
+
// Response management
|
|
325
|
+
setResponse,
|
|
326
|
+
setLoading,
|
|
327
|
+
|
|
328
|
+
// UI management
|
|
329
|
+
setSidebarOpen,
|
|
330
|
+
|
|
331
|
+
// Actions
|
|
332
|
+
clearAll,
|
|
333
|
+
copyToClipboard,
|
|
334
|
+
sendRequest,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
return <PlaygroundContext.Provider value={contextValue}>{children}</PlaygroundContext.Provider>;
|
|
338
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import consola from 'consola';
|
|
5
|
+
import {
|
|
6
|
+
ApiEndpoint,
|
|
7
|
+
OpenApiSchema,
|
|
8
|
+
SchemaSource,
|
|
9
|
+
UseOpenApiSchemaReturn,
|
|
10
|
+
} from '../types';
|
|
11
|
+
|
|
12
|
+
// Extract endpoints from OpenAPI schema (GET only)
|
|
13
|
+
const extractEndpoints = (schema: OpenApiSchema): ApiEndpoint[] => {
|
|
14
|
+
const endpointMap = new Map<string, ApiEndpoint>();
|
|
15
|
+
|
|
16
|
+
if (!schema.paths) return [];
|
|
17
|
+
|
|
18
|
+
// Get base URL from servers
|
|
19
|
+
const baseUrl = schema.servers && schema.servers.length > 0 ? schema.servers[0].url : '';
|
|
20
|
+
|
|
21
|
+
for (const [path, methods] of Object.entries(schema.paths)) {
|
|
22
|
+
// Only process GET methods
|
|
23
|
+
const getOperation = (methods as any).get;
|
|
24
|
+
if (!getOperation) continue;
|
|
25
|
+
|
|
26
|
+
const op = getOperation as any;
|
|
27
|
+
const description = op.description || op.summary || `GET ${path}`;
|
|
28
|
+
const category = op.tags?.[0] || 'Other';
|
|
29
|
+
|
|
30
|
+
const parameters: Array<{
|
|
31
|
+
name: string;
|
|
32
|
+
type: string;
|
|
33
|
+
required: boolean;
|
|
34
|
+
description?: string;
|
|
35
|
+
}> = [];
|
|
36
|
+
|
|
37
|
+
// Collect parameters
|
|
38
|
+
if (op.parameters) {
|
|
39
|
+
for (const param of op.parameters) {
|
|
40
|
+
parameters.push({
|
|
41
|
+
name: param.name,
|
|
42
|
+
type: param.schema?.type || 'string',
|
|
43
|
+
required: param.required || false,
|
|
44
|
+
description: param.description,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Collect responses
|
|
50
|
+
const responses: Array<{
|
|
51
|
+
code: string;
|
|
52
|
+
description: string;
|
|
53
|
+
}> = [];
|
|
54
|
+
|
|
55
|
+
if (op.responses) {
|
|
56
|
+
for (const [code, response] of Object.entries(op.responses)) {
|
|
57
|
+
responses.push({
|
|
58
|
+
code,
|
|
59
|
+
description: (response as any).description || `Response ${code}`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Create endpoint (GET only) with full URL
|
|
65
|
+
const endpoint: ApiEndpoint = {
|
|
66
|
+
name: path.split('/').pop() || path,
|
|
67
|
+
method: 'GET',
|
|
68
|
+
path: baseUrl + path, // Combine baseUrl with path
|
|
69
|
+
description,
|
|
70
|
+
category,
|
|
71
|
+
parameters: parameters.length > 0 ? parameters : undefined,
|
|
72
|
+
requestBody: undefined, // GET requests don't have request body
|
|
73
|
+
responses: responses.length > 0 ? responses : undefined,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
endpointMap.set(path, endpoint);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return Array.from(endpointMap.values());
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Get unique categories from endpoints
|
|
83
|
+
const getCategories = (endpoints: ApiEndpoint[]): string[] => {
|
|
84
|
+
const categories = new Set<string>();
|
|
85
|
+
endpoints.forEach((endpoint) => categories.add(endpoint.category));
|
|
86
|
+
return Array.from(categories).sort();
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Fetch schema from URL
|
|
90
|
+
const fetchSchema = async (url: string): Promise<OpenApiSchema> => {
|
|
91
|
+
const response = await fetch(url, {
|
|
92
|
+
headers: {
|
|
93
|
+
'Accept': 'application/json',
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
throw new Error(`Failed to fetch schema: ${response.statusText}`);
|
|
98
|
+
}
|
|
99
|
+
return response.json();
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
interface UseOpenApiSchemaProps {
|
|
103
|
+
schemas: SchemaSource[];
|
|
104
|
+
defaultSchemaId?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default function useOpenApiSchema({
|
|
108
|
+
schemas,
|
|
109
|
+
defaultSchemaId,
|
|
110
|
+
}: UseOpenApiSchemaProps): UseOpenApiSchemaReturn {
|
|
111
|
+
const [loading, setLoading] = useState(true);
|
|
112
|
+
const [error, setError] = useState<string | null>(null);
|
|
113
|
+
const [currentSchemaId, setCurrentSchemaId] = useState<string>(
|
|
114
|
+
defaultSchemaId || schemas[0]?.id
|
|
115
|
+
);
|
|
116
|
+
const [loadedSchemas, setLoadedSchemas] = useState<Map<string, OpenApiSchema>>(
|
|
117
|
+
new Map()
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const currentSchema = useMemo(
|
|
121
|
+
() => schemas.find((s) => s.id === currentSchemaId) || null,
|
|
122
|
+
[schemas, currentSchemaId]
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const currentOpenApiSchema = useMemo(
|
|
126
|
+
() => (currentSchemaId ? loadedSchemas.get(currentSchemaId) : null),
|
|
127
|
+
[loadedSchemas, currentSchemaId]
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const endpoints = useMemo(
|
|
131
|
+
() => (currentOpenApiSchema ? extractEndpoints(currentOpenApiSchema) : []),
|
|
132
|
+
[currentOpenApiSchema]
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const categories = useMemo(() => getCategories(endpoints), [endpoints]);
|
|
136
|
+
|
|
137
|
+
// Load schema when current schema changes
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (!currentSchema) return;
|
|
140
|
+
|
|
141
|
+
// Skip if already loaded
|
|
142
|
+
if (loadedSchemas.has(currentSchema.id)) {
|
|
143
|
+
setLoading(false);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
setLoading(true);
|
|
148
|
+
setError(null);
|
|
149
|
+
|
|
150
|
+
fetchSchema(currentSchema.url)
|
|
151
|
+
.then((schema) => {
|
|
152
|
+
setLoadedSchemas((prev) => new Map(prev).set(currentSchema.id, schema));
|
|
153
|
+
consola.success(`Schema loaded: ${currentSchema.name}`);
|
|
154
|
+
setLoading(false);
|
|
155
|
+
})
|
|
156
|
+
.catch((err) => {
|
|
157
|
+
consola.error(`Error loading schema from ${currentSchema.url}:`, err);
|
|
158
|
+
setError(err instanceof Error ? err.message : 'Failed to load schema');
|
|
159
|
+
setLoading(false);
|
|
160
|
+
});
|
|
161
|
+
}, [currentSchema, loadedSchemas]);
|
|
162
|
+
|
|
163
|
+
const setCurrentSchema = useCallback((schemaId: string) => {
|
|
164
|
+
setCurrentSchemaId(schemaId);
|
|
165
|
+
}, []);
|
|
166
|
+
|
|
167
|
+
const refresh = useCallback(() => {
|
|
168
|
+
if (!currentSchema) return;
|
|
169
|
+
|
|
170
|
+
setLoading(true);
|
|
171
|
+
setError(null);
|
|
172
|
+
|
|
173
|
+
// Remove from cache to force reload
|
|
174
|
+
setLoadedSchemas((prev) => {
|
|
175
|
+
const next = new Map(prev);
|
|
176
|
+
next.delete(currentSchema.id);
|
|
177
|
+
return next;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
fetchSchema(currentSchema.url)
|
|
181
|
+
.then((schema) => {
|
|
182
|
+
setLoadedSchemas((prev) => new Map(prev).set(currentSchema.id, schema));
|
|
183
|
+
consola.success(`Schema refreshed: ${currentSchema.name}`);
|
|
184
|
+
setLoading(false);
|
|
185
|
+
})
|
|
186
|
+
.catch((err) => {
|
|
187
|
+
consola.error(`Error refreshing schema from ${currentSchema.url}:`, err);
|
|
188
|
+
setError(err instanceof Error ? err.message : 'Failed to refresh schema');
|
|
189
|
+
setLoading(false);
|
|
190
|
+
});
|
|
191
|
+
}, [currentSchema]);
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
loading,
|
|
195
|
+
error,
|
|
196
|
+
endpoints,
|
|
197
|
+
categories,
|
|
198
|
+
schemas,
|
|
199
|
+
currentSchema,
|
|
200
|
+
setCurrentSchema,
|
|
201
|
+
refresh,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import dynamic from 'next/dynamic';
|
|
5
|
+
import { PlaygroundProvider } from './context/PlaygroundContext';
|
|
6
|
+
import type { PlaygroundConfig } from './types';
|
|
7
|
+
|
|
8
|
+
const PlaygroundLayout = dynamic(
|
|
9
|
+
() => import('./components/PlaygroundLayout').then((mod) => ({ default: mod.PlaygroundLayout })),
|
|
10
|
+
{
|
|
11
|
+
ssr: false,
|
|
12
|
+
loading: () => (
|
|
13
|
+
<div className="flex items-center justify-center min-h-[400px]">
|
|
14
|
+
<div className="text-muted-foreground">Loading API Playground...</div>
|
|
15
|
+
</div>
|
|
16
|
+
),
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export interface PlaygroundProps {
|
|
21
|
+
config: PlaygroundConfig;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const Playground: React.FC<PlaygroundProps> = ({ config }) => {
|
|
25
|
+
return (
|
|
26
|
+
<PlaygroundProvider config={config}>
|
|
27
|
+
<PlaygroundLayout />
|
|
28
|
+
</PlaygroundProvider>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Re-export types for convenience
|
|
33
|
+
export type { PlaygroundConfig, SchemaSource } from './types';
|
|
34
|
+
|
|
35
|
+
// Default export for dynamic import
|
|
36
|
+
export default Playground;
|