@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.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/package.json +110 -0
  4. package/src/animations/AnimatedBackground.tsx +645 -0
  5. package/src/animations/index.ts +2 -0
  6. package/src/blocks/ArticleCard.tsx +94 -0
  7. package/src/blocks/ArticleList.tsx +95 -0
  8. package/src/blocks/CTASection.tsx +136 -0
  9. package/src/blocks/FeatureSection.tsx +104 -0
  10. package/src/blocks/Hero.tsx +102 -0
  11. package/src/blocks/NewsletterSection.tsx +119 -0
  12. package/src/blocks/StatsSection.tsx +103 -0
  13. package/src/blocks/SuperHero.tsx +328 -0
  14. package/src/blocks/TestimonialSection.tsx +122 -0
  15. package/src/blocks/index.ts +9 -0
  16. package/src/components/README.md +2018 -0
  17. package/src/components/breadcrumb-navigation.tsx +127 -0
  18. package/src/components/breadcrumb.tsx +132 -0
  19. package/src/components/button-download.tsx +275 -0
  20. package/src/components/dropdown-menu.tsx +219 -0
  21. package/src/components/index.ts +86 -0
  22. package/src/components/markdown/MarkdownMessage.tsx +338 -0
  23. package/src/components/markdown/index.ts +5 -0
  24. package/src/components/menubar.tsx +274 -0
  25. package/src/components/multi-select-pro/async.tsx +608 -0
  26. package/src/components/multi-select-pro/helpers.tsx +84 -0
  27. package/src/components/multi-select-pro/index.tsx +622 -0
  28. package/src/components/navigation-menu.tsx +153 -0
  29. package/src/components/pagination-static.tsx +348 -0
  30. package/src/components/pagination.tsx +138 -0
  31. package/src/components/phone-input.tsx +276 -0
  32. package/src/components/sidebar.tsx +866 -0
  33. package/src/components/sonner.tsx +31 -0
  34. package/src/components/ssr-pagination.tsx +237 -0
  35. package/src/hooks/index.ts +19 -0
  36. package/src/hooks/useCfgRouter.ts +153 -0
  37. package/src/hooks/useLocalStorage.ts +221 -0
  38. package/src/hooks/useQueryParams.ts +73 -0
  39. package/src/hooks/useSessionStorage.ts +188 -0
  40. package/src/hooks/useTheme.ts +57 -0
  41. package/src/index.ts +24 -0
  42. package/src/lib/index.ts +2 -0
  43. package/src/styles/index.css +2 -0
  44. package/src/theme/ForceTheme.tsx +115 -0
  45. package/src/theme/ThemeProvider.tsx +82 -0
  46. package/src/theme/ThemeToggle.tsx +52 -0
  47. package/src/theme/index.ts +3 -0
  48. package/src/tools/JsonForm/JsonSchemaForm.tsx +199 -0
  49. package/src/tools/JsonForm/examples/BotConfigExample.tsx +245 -0
  50. package/src/tools/JsonForm/examples/RealBotConfigExample.tsx +157 -0
  51. package/src/tools/JsonForm/index.ts +46 -0
  52. package/src/tools/JsonForm/templates/ArrayFieldItemTemplate.tsx +46 -0
  53. package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +73 -0
  54. package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +106 -0
  55. package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +34 -0
  56. package/src/tools/JsonForm/templates/FieldTemplate.tsx +61 -0
  57. package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +43 -0
  58. package/src/tools/JsonForm/templates/index.ts +12 -0
  59. package/src/tools/JsonForm/types.ts +83 -0
  60. package/src/tools/JsonForm/utils.ts +212 -0
  61. package/src/tools/JsonForm/widgets/CheckboxWidget.tsx +36 -0
  62. package/src/tools/JsonForm/widgets/NumberWidget.tsx +88 -0
  63. package/src/tools/JsonForm/widgets/SelectWidget.tsx +100 -0
  64. package/src/tools/JsonForm/widgets/SwitchWidget.tsx +34 -0
  65. package/src/tools/JsonForm/widgets/TextWidget.tsx +95 -0
  66. package/src/tools/JsonForm/widgets/index.ts +12 -0
  67. package/src/tools/JsonTree/index.tsx +252 -0
  68. package/src/tools/LottiePlayer/LottiePlayer.client.tsx +212 -0
  69. package/src/tools/LottiePlayer/index.tsx +54 -0
  70. package/src/tools/LottiePlayer/types.ts +108 -0
  71. package/src/tools/LottiePlayer/useLottie.ts +163 -0
  72. package/src/tools/Mermaid/Mermaid.client.tsx +341 -0
  73. package/src/tools/Mermaid/index.tsx +40 -0
  74. package/src/tools/OpenapiViewer/components/EndpointInfo.tsx +144 -0
  75. package/src/tools/OpenapiViewer/components/EndpointsLibrary.tsx +255 -0
  76. package/src/tools/OpenapiViewer/components/PlaygroundLayout.tsx +123 -0
  77. package/src/tools/OpenapiViewer/components/PlaygroundStepper.tsx +98 -0
  78. package/src/tools/OpenapiViewer/components/RequestBuilder.tsx +164 -0
  79. package/src/tools/OpenapiViewer/components/RequestParametersForm.tsx +253 -0
  80. package/src/tools/OpenapiViewer/components/ResponseViewer.tsx +169 -0
  81. package/src/tools/OpenapiViewer/components/VersionSelector.tsx +64 -0
  82. package/src/tools/OpenapiViewer/components/index.ts +14 -0
  83. package/src/tools/OpenapiViewer/constants.ts +39 -0
  84. package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +338 -0
  85. package/src/tools/OpenapiViewer/hooks/index.ts +8 -0
  86. package/src/tools/OpenapiViewer/hooks/useMobile.ts +10 -0
  87. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +203 -0
  88. package/src/tools/OpenapiViewer/index.tsx +36 -0
  89. package/src/tools/OpenapiViewer/types.ts +152 -0
  90. package/src/tools/OpenapiViewer/utils/apiKeyManager.ts +149 -0
  91. package/src/tools/OpenapiViewer/utils/formatters.ts +71 -0
  92. package/src/tools/OpenapiViewer/utils/index.ts +9 -0
  93. package/src/tools/OpenapiViewer/utils/versionManager.ts +161 -0
  94. package/src/tools/PrettyCode/PrettyCode.client.tsx +217 -0
  95. package/src/tools/PrettyCode/index.tsx +43 -0
  96. package/src/tools/VideoPlayer/README.md +239 -0
  97. package/src/tools/VideoPlayer/VideoControls.tsx +138 -0
  98. package/src/tools/VideoPlayer/VideoPlayer.tsx +230 -0
  99. package/src/tools/VideoPlayer/index.ts +9 -0
  100. package/src/tools/VideoPlayer/types.ts +62 -0
  101. 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,8 @@
1
+ /**
2
+ * Playground Hooks
3
+ *
4
+ * Centralized exports for all playground hooks
5
+ */
6
+
7
+ export { default as useOpenApiSchema } from './useOpenApiSchema';
8
+ export { useMobile } from './useMobile';
@@ -0,0 +1,10 @@
1
+ import { useIsMobile } from '@djangocfg/ui-core/hooks';
2
+
3
+ export const useMobile = () => {
4
+ const isMobile = useIsMobile();
5
+
6
+ return {
7
+ isMobile,
8
+ isDesktop: !isMobile,
9
+ };
10
+ };
@@ -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;