@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,255 @@
1
+ 'use client';
2
+
3
+ import React, { useMemo } from 'react';
4
+ import { Badge, Button, Card, CardContent, CardHeader, CardTitle, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Skeleton } from '@djangocfg/ui-core/components';
5
+ import { Search, Code, Database, Shield, Settings, Users, FileText, BarChart3, Filter, Grid3X3, List } from 'lucide-react';
6
+ import { usePlaygroundContext } from '../context/PlaygroundContext';
7
+ import type { ApiEndpoint } from '../types';
8
+ import { getMethodColor } from '../utils';
9
+ import useOpenApiSchema from '../hooks/useOpenApiSchema';
10
+ import { deduplicateEndpoints } from '../utils/versionManager';
11
+ import { VersionSelector } from './VersionSelector';
12
+
13
+ const categoryIcons: Record<string, React.ReactNode> = {
14
+ 'Authentication': <Shield className="h-4 w-4" />,
15
+ 'Users': <Users className="h-4 w-4" />,
16
+ 'Data': <Database className="h-4 w-4" />,
17
+ 'Analytics': <BarChart3 className="h-4 w-4" />,
18
+ 'Files': <FileText className="h-4 w-4" />,
19
+ 'Settings': <Settings className="h-4 w-4" />,
20
+ 'Other': <Code className="h-4 w-4" />,
21
+ };
22
+
23
+ export const EndpointsLibrary: React.FC = () => {
24
+ const { state, config, setSelectedEndpoint, setSelectedCategory, setSearchTerm } = usePlaygroundContext();
25
+ const { endpoints, categories, loading, error } = useOpenApiSchema({
26
+ schemas: config.schemas,
27
+ defaultSchemaId: config.defaultSchemaId,
28
+ });
29
+ const [viewMode, setViewMode] = React.useState<'table' | 'grid'>('table');
30
+
31
+ // Helper to extract relative path from full URL
32
+ const getRelativePath = (fullPath: string): string => {
33
+ try {
34
+ const url = new URL(fullPath);
35
+ return url.pathname;
36
+ } catch {
37
+ // If not a valid URL, return as is (already relative)
38
+ return fullPath;
39
+ }
40
+ };
41
+
42
+ const filteredEndpoints = useMemo(() => {
43
+ // First, deduplicate endpoints based on selected version
44
+ let filtered = deduplicateEndpoints(endpoints, state.selectedVersion);
45
+
46
+ // Filter by category
47
+ if (state.selectedCategory && state.selectedCategory !== 'All') {
48
+ filtered = filtered.filter((endpoint) => endpoint.category === state.selectedCategory);
49
+ }
50
+
51
+ // Filter by search term
52
+ if (state.searchTerm) {
53
+ const searchLower = state.searchTerm.toLowerCase();
54
+ filtered = filtered.filter(
55
+ (endpoint) =>
56
+ endpoint.name.toLowerCase().includes(searchLower) ||
57
+ endpoint.description.toLowerCase().includes(searchLower) ||
58
+ endpoint.path.toLowerCase().includes(searchLower)
59
+ );
60
+ }
61
+
62
+ return filtered;
63
+ }, [endpoints, state.selectedCategory, state.searchTerm, state.selectedVersion]);
64
+
65
+ const handleEndpointSelect = (endpoint: ApiEndpoint) => {
66
+ setSelectedEndpoint(endpoint);
67
+ };
68
+
69
+ const getMethodBadges = (methods: string) => {
70
+ return methods.split(', ').map((method) => (
71
+ <Badge key={method} variant={getMethodColor(method) === 'success' ? 'default' : 'secondary'} className="text-xs">
72
+ {method}
73
+ </Badge>
74
+ ));
75
+ };
76
+
77
+ if (loading) {
78
+ return (
79
+ <div className="space-y-4">
80
+ <div className="flex items-center justify-between">
81
+ <h2 className="text-lg font-semibold text-foreground">API Endpoints</h2>
82
+ <div className="flex items-center space-x-2">
83
+ <Skeleton className="h-8 w-32" />
84
+ <Skeleton className="h-8 w-48" />
85
+ </div>
86
+ </div>
87
+ <div className="space-y-2">
88
+ {Array.from({ length: 5 }).map((_, i) => (
89
+ <Skeleton key={i} className="h-20 w-full" />
90
+ ))}
91
+ </div>
92
+ </div>
93
+ );
94
+ }
95
+
96
+ if (error) {
97
+ return (
98
+ <div className="space-y-4">
99
+ <div className="flex items-center justify-between">
100
+ <h2 className="text-lg font-semibold text-foreground">API Endpoints</h2>
101
+ </div>
102
+ <Card className="bg-destructive/10 border-destructive/20">
103
+ <CardContent className="p-4">
104
+ <p className="text-sm text-destructive">Error loading endpoints: {error}</p>
105
+ </CardContent>
106
+ </Card>
107
+ </div>
108
+ );
109
+ }
110
+
111
+ return (
112
+ <div className="space-y-4">
113
+ {/* Header */}
114
+ <div className="flex flex-col gap-4">
115
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
116
+ <h2 className="text-lg font-semibold text-foreground">API Endpoints</h2>
117
+ <VersionSelector />
118
+ </div>
119
+
120
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
121
+ <div className="flex items-center space-x-2">
122
+ {/* View Mode Toggle */}
123
+ <div className="flex items-center border rounded-md">
124
+ <Button
125
+ variant={viewMode === 'table' ? 'default' : 'ghost'}
126
+ size="sm"
127
+ onClick={() => setViewMode('table')}
128
+ className="rounded-r-none"
129
+ >
130
+ <List className="h-4 w-4" />
131
+ </Button>
132
+ <Button
133
+ variant={viewMode === 'grid' ? 'default' : 'ghost'}
134
+ size="sm"
135
+ onClick={() => setViewMode('grid')}
136
+ className="rounded-l-none"
137
+ >
138
+ <Grid3X3 className="h-4 w-4" />
139
+ </Button>
140
+ </div>
141
+
142
+ {/* Category Filter */}
143
+ <Select value={state.selectedCategory} onValueChange={setSelectedCategory}>
144
+ <SelectTrigger className="w-32">
145
+ <Filter className="h-4 w-4 mr-2" />
146
+ <SelectValue />
147
+ </SelectTrigger>
148
+ <SelectContent>
149
+ <SelectItem value="All">All</SelectItem>
150
+ {categories.map((category) => (
151
+ <SelectItem key={category} value={category}>
152
+ {category}
153
+ </SelectItem>
154
+ ))}
155
+ </SelectContent>
156
+ </Select>
157
+
158
+ {/* Search */}
159
+ <div className="relative">
160
+ <Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
161
+ <Input
162
+ placeholder="Search endpoints..."
163
+ value={state.searchTerm}
164
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
165
+ className="w-48 pl-8"
166
+ />
167
+ </div>
168
+ </div>
169
+ </div>
170
+ </div>
171
+
172
+ {/* Content */}
173
+ {viewMode === 'table' ? (
174
+ <Card>
175
+ <Table>
176
+ <TableHeader>
177
+ <TableRow>
178
+ <TableHead className="text-foreground">Methods</TableHead>
179
+ <TableHead className="text-foreground">Path</TableHead>
180
+ <TableHead className="text-foreground">Description</TableHead>
181
+ </TableRow>
182
+ </TableHeader>
183
+ <TableBody>
184
+ {filteredEndpoints.length === 0 ? (
185
+ <TableRow>
186
+ <TableCell colSpan={3} className="text-center py-8 text-muted-foreground">
187
+ No endpoints found
188
+ </TableCell>
189
+ </TableRow>
190
+ ) : (
191
+ filteredEndpoints.map((endpoint) => (
192
+ <TableRow
193
+ key={`${endpoint.method}-${endpoint.path}`}
194
+ className={`cursor-pointer transition-colors hover:bg-muted/50 ${state.selectedEndpoint?.path === endpoint.path ? 'bg-primary/10' : ''
195
+ }`}
196
+ onClick={() => handleEndpointSelect(endpoint)}
197
+ >
198
+ <TableCell>
199
+ <div className="flex space-x-1">
200
+ {getMethodBadges(endpoint.method)}
201
+ </div>
202
+ </TableCell>
203
+ <TableCell className="font-mono text-sm text-muted-foreground">
204
+ {getRelativePath(endpoint.path)}
205
+ </TableCell>
206
+ <TableCell className="text-sm text-muted-foreground max-w-xs truncate">
207
+ {endpoint.description}
208
+ </TableCell>
209
+ </TableRow>
210
+ ))
211
+ )}
212
+ </TableBody>
213
+ </Table>
214
+ </Card>
215
+ ) : (
216
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
217
+ {filteredEndpoints.length === 0 ? (
218
+ <div className="col-span-full text-center py-8 text-muted-foreground">
219
+ No endpoints found
220
+ </div>
221
+ ) : (
222
+ filteredEndpoints.map((endpoint) => (
223
+ <Card
224
+ key={`${endpoint.method}-${endpoint.path}`}
225
+ className={`cursor-pointer transition-all hover:shadow-md ${state.selectedEndpoint?.path === endpoint.path ? 'ring-2 ring-primary' : ''
226
+ }`}
227
+ onClick={() => handleEndpointSelect(endpoint)}
228
+ >
229
+ <CardHeader className="pb-3">
230
+ <CardTitle className="flex items-center justify-between text-sm">
231
+ <div className="flex items-center space-x-2">
232
+ {categoryIcons[endpoint.category] || <Code className="h-4 w-4" />}
233
+ <span className="truncate">{endpoint.name}</span>
234
+ </div>
235
+ <div className="flex space-x-1">
236
+ {getMethodBadges(endpoint.method)}
237
+ </div>
238
+ </CardTitle>
239
+ </CardHeader>
240
+ <CardContent className="space-y-2">
241
+ <p className="text-xs font-mono text-muted-foreground break-all">
242
+ {getRelativePath(endpoint.path)}
243
+ </p>
244
+ <p className="text-xs text-muted-foreground line-clamp-2">
245
+ {endpoint.description}
246
+ </p>
247
+ </CardContent>
248
+ </Card>
249
+ ))
250
+ )}
251
+ </div>
252
+ )}
253
+ </div>
254
+ );
255
+ };
@@ -0,0 +1,123 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { Button, Sheet, SheetContent, SheetTrigger } from '@djangocfg/ui-core/components';
5
+ import { Menu, X } from 'lucide-react';
6
+ import { usePlaygroundContext } from '../context/PlaygroundContext';
7
+ import { useMobile } from '../hooks/useMobile';
8
+ import { PlaygroundStepper } from './PlaygroundStepper';
9
+ import { EndpointsLibrary } from './EndpointsLibrary';
10
+ import { RequestBuilder } from './RequestBuilder';
11
+ import { ResponseViewer } from './ResponseViewer';
12
+
13
+ export const PlaygroundLayout: React.FC = () => {
14
+ const { state, setSidebarOpen } = usePlaygroundContext();
15
+ const { isMobile } = useMobile();
16
+
17
+ const renderStepContent = () => {
18
+ switch (state.currentStep) {
19
+ case 'endpoints':
20
+ return <EndpointsLibrary />;
21
+ case 'request':
22
+ return <RequestBuilder />;
23
+ case 'response':
24
+ return <ResponseViewer />;
25
+ default:
26
+ return <EndpointsLibrary />;
27
+ }
28
+ };
29
+
30
+ const getStepTitle = () => {
31
+ switch (state.currentStep) {
32
+ case 'endpoints':
33
+ return 'API Endpoints';
34
+ case 'request':
35
+ return 'Request Builder';
36
+ case 'response':
37
+ return 'Response Viewer';
38
+ default:
39
+ return 'API Playground';
40
+ }
41
+ };
42
+
43
+ return (
44
+ <div className="min-h-screen">
45
+ {/* Header */}
46
+ <div className="border-b">
47
+ <div className="container mx-auto px-4 py-4">
48
+ <div className="flex items-center justify-between">
49
+ <div className="flex items-center space-x-4">
50
+ <h1 className="text-xl font-bold text-foreground">API Playground</h1>
51
+ <p className="text-sm text-muted-foreground hidden sm:block">
52
+ Test and explore API endpoints
53
+ </p>
54
+ </div>
55
+
56
+ {/* Mobile Menu */}
57
+ {isMobile && (
58
+ <Sheet open={state.sidebarOpen} onOpenChange={setSidebarOpen}>
59
+ <SheetTrigger asChild>
60
+ <Button variant="outline" size="sm">
61
+ <Menu className="h-4 w-4" />
62
+ </Button>
63
+ </SheetTrigger>
64
+ <SheetContent side="left" className="w-80">
65
+ <div className="space-y-4">
66
+ <div className="flex items-center justify-between">
67
+ <h2 className="text-lg font-semibold text-foreground">Navigation</h2>
68
+ <Button
69
+ variant="ghost"
70
+ size="sm"
71
+ onClick={() => setSidebarOpen(false)}
72
+ >
73
+ <X className="h-4 w-4" />
74
+ </Button>
75
+ </div>
76
+ <PlaygroundStepper />
77
+ </div>
78
+ </SheetContent>
79
+ </Sheet>
80
+ )}
81
+ </div>
82
+ </div>
83
+ </div>
84
+
85
+ {/* Desktop Stepper */}
86
+ {!isMobile && <PlaygroundStepper />}
87
+
88
+ {/* Main Content */}
89
+ <div className="container mx-auto px-6 py-6">
90
+ {/* Step Title */}
91
+ <div className="mb-6">
92
+ <h2 className="text-2xl font-bold text-foreground">{getStepTitle()}</h2>
93
+ <p className="text-muted-foreground mt-1">
94
+ {state.currentStep === 'endpoints' && 'Browse and select API endpoints'}
95
+ {state.currentStep === 'request' && 'Configure your API request'}
96
+ {state.currentStep === 'response' && 'View API response and details'}
97
+ </p>
98
+ </div>
99
+
100
+ {/* Content */}
101
+ <div className="space-y-6">
102
+ {renderStepContent()}
103
+ </div>
104
+ </div>
105
+
106
+ {/* Mobile Floating Action Button */}
107
+ {isMobile && state.currentStep === 'request' && (
108
+ <div className="fixed bottom-4 right-4 z-50">
109
+ <Button
110
+ size="lg"
111
+ className="rounded-full shadow-lg"
112
+ onClick={() => {
113
+ // Auto-advance to response step
114
+ // This would be handled by the context
115
+ }}
116
+ >
117
+ Send Request
118
+ </Button>
119
+ </div>
120
+ )}
121
+ </div>
122
+ );
123
+ };
@@ -0,0 +1,98 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { Badge, Button } from '@djangocfg/ui-core/components';
5
+ import { ChevronLeft, ChevronRight, Check, Code, Send, FileText } from 'lucide-react';
6
+ import { usePlaygroundContext } from '../context/PlaygroundContext';
7
+ import { PlaygroundStep } from '../types';
8
+
9
+ const stepConfig = {
10
+ endpoints: {
11
+ title: 'Endpoints',
12
+ icon: Code,
13
+ description: 'Select API endpoint'
14
+ },
15
+ request: {
16
+ title: 'Request',
17
+ icon: Send,
18
+ description: 'Configure request'
19
+ },
20
+ response: {
21
+ title: 'Response',
22
+ icon: FileText,
23
+ description: 'View response'
24
+ }
25
+ };
26
+
27
+ export const PlaygroundStepper: React.FC = () => {
28
+ const { state, setCurrentStep, goToNextStep, goToPreviousStep } = usePlaygroundContext();
29
+ const { currentStep, steps } = state;
30
+
31
+ const currentIndex = steps.indexOf(currentStep);
32
+ const canGoNext = currentIndex < steps.length - 1;
33
+ const canGoPrevious = currentIndex > 0;
34
+
35
+ return (
36
+ <div className="flex items-center justify-between p-4 border-b">
37
+ {/* Steps */}
38
+ <div className="flex items-center space-x-4">
39
+ {steps.map((step, index) => {
40
+ const config = stepConfig[step];
41
+ const Icon = config.icon;
42
+ const isActive = step === currentStep;
43
+ const isCompleted = index < currentIndex;
44
+ const isClickable = index <= currentIndex + 1;
45
+
46
+ return (
47
+ <div key={step} className="flex items-center space-x-2">
48
+ <Button
49
+ variant={isActive ? 'default' : 'ghost'}
50
+ size="sm"
51
+ onClick={() => isClickable && setCurrentStep(step)}
52
+ className={`flex items-center space-x-2 ${isClickable ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'
53
+ }`}
54
+ disabled={!isClickable}
55
+ >
56
+ {isCompleted ? (
57
+ <Check className="h-4 w-4" />
58
+ ) : (
59
+ <Icon className="h-4 w-4" />
60
+ )}
61
+ <span className="hidden sm:inline">{config.title}</span>
62
+ </Button>
63
+
64
+ {index < steps.length - 1 && (
65
+ <div className="w-8 h-px bg-border" />
66
+ )}
67
+ </div>
68
+ );
69
+ })}
70
+ </div>
71
+
72
+ {/* Navigation */}
73
+ <div className="flex items-center space-x-2">
74
+ <Button
75
+ variant="outline"
76
+ size="sm"
77
+ onClick={goToPreviousStep}
78
+ disabled={!canGoPrevious}
79
+ className="flex items-center space-x-1"
80
+ >
81
+ <ChevronLeft className="h-4 w-4" />
82
+ <span className="hidden sm:inline">Previous</span>
83
+ </Button>
84
+
85
+ <Button
86
+ variant="outline"
87
+ size="sm"
88
+ onClick={goToNextStep}
89
+ disabled={!canGoNext}
90
+ className="flex items-center space-x-1"
91
+ >
92
+ <span className="hidden sm:inline">Next</span>
93
+ <ChevronRight className="h-4 w-4" />
94
+ </Button>
95
+ </div>
96
+ </div>
97
+ );
98
+ };
@@ -0,0 +1,164 @@
1
+ 'use client';
2
+
3
+ import React, { useCallback } from 'react';
4
+ import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, Badge, Skeleton } from '@djangocfg/ui-core/components';
5
+ import { Play, Copy, RotateCcw, Loader2, Key, Settings, Send } from 'lucide-react';
6
+ import { usePlaygroundContext } from '../context/PlaygroundContext';
7
+ import { findApiKeyById, logApiKeyUsage } from '../utils';
8
+ import { isValidJson, parseRequestHeaders } from '../utils';
9
+ import { RequestParametersForm } from './RequestParametersForm';
10
+ import { EndpointInfo } from './EndpointInfo';
11
+ import PrettyCode from '../../PrettyCode';
12
+
13
+ export const RequestBuilder: React.FC = () => {
14
+ const {
15
+ state,
16
+ apiKeys,
17
+ apiKeysLoading,
18
+ setRequestUrl,
19
+ setRequestMethod,
20
+ setRequestHeaders,
21
+ setRequestBody,
22
+ setResponse,
23
+ setLoading,
24
+ setSelectedApiKey,
25
+ setManualApiToken,
26
+ copyToClipboard,
27
+ sendRequest
28
+ } = usePlaygroundContext();
29
+
30
+ const isJsonValid = state.requestBody ? isValidJson(state.requestBody) : true;
31
+
32
+ // Generate cURL command
33
+ const generateCurlCommand = () => {
34
+ if (!state.requestUrl) return '';
35
+
36
+ const apiKey = state.selectedApiKey ? findApiKeyById(apiKeys, state.selectedApiKey) : null;
37
+ const headers = parseRequestHeaders(state.requestHeaders);
38
+
39
+ if (apiKey) {
40
+ headers['X-API-Key'] = apiKey.id || ''; // Note: using id as full key not available
41
+ }
42
+
43
+ let curl = `curl -X ${state.requestMethod} "${state.requestUrl}"`;
44
+
45
+ // Add headers
46
+ Object.entries(headers).forEach(([key, value]) => {
47
+ curl += ` \\\n -H "${key}: ${value}"`;
48
+ });
49
+
50
+ // Add body for non-GET requests
51
+ if (state.requestBody && state.requestMethod !== 'GET' && isJsonValid) {
52
+ curl += ` \\\n -d '${state.requestBody}'`;
53
+ }
54
+
55
+ return curl;
56
+ };
57
+
58
+ const curlCommand = generateCurlCommand();
59
+
60
+ const handleSendRequest = useCallback(async () => {
61
+ await sendRequest();
62
+ }, [sendRequest]);
63
+
64
+ return (
65
+ <div className="space-y-4">
66
+ {/* Endpoint Info */}
67
+ {state.selectedEndpoint && <EndpointInfo />}
68
+
69
+ {/* Request Parameters Form */}
70
+ {state.selectedEndpoint && (
71
+ <RequestParametersForm />
72
+ )}
73
+
74
+ {/* Request Body */}
75
+ {state.requestMethod !== 'GET' && (
76
+ <Card className="">
77
+ <CardHeader>
78
+ <CardTitle className="text-sm text-foreground">Request Body</CardTitle>
79
+ </CardHeader>
80
+ <CardContent>
81
+ <Textarea
82
+ placeholder='{\n "key": "value"\n}'
83
+ value={state.requestBody}
84
+ onChange={(e) => setRequestBody(e.target.value)}
85
+ className={`font-mono text-sm ${
86
+ !isJsonValid ? 'border-destructive' : ''
87
+ }`}
88
+ rows={6}
89
+ />
90
+ {!isJsonValid && (
91
+ <p className="mt-1 text-xs text-destructive">Invalid JSON format</p>
92
+ )}
93
+ </CardContent>
94
+ </Card>
95
+ )}
96
+
97
+ {/* Bearer Token Authentication */}
98
+ <Card className="">
99
+ <CardHeader>
100
+ <CardTitle className="flex items-center space-x-2 text-sm text-foreground">
101
+ <Key className="h-4 w-4" />
102
+ <span>Bearer Token</span>
103
+ </CardTitle>
104
+ </CardHeader>
105
+ <CardContent>
106
+ <div className="flex flex-col sm:flex-row sm:items-center gap-2">
107
+ <div className="flex-1">
108
+ <Input
109
+ type="password"
110
+ placeholder="Enter Bearer token (optional, uses JWT if empty)"
111
+ value={state.manualApiToken}
112
+ onChange={(e) => setManualApiToken(e.target.value)}
113
+ className="font-mono text-sm"
114
+ />
115
+ <p className="mt-1 text-xs text-muted-foreground">
116
+ Leave empty to use JWT token from localStorage
117
+ </p>
118
+ </div>
119
+ <Button
120
+ onClick={handleSendRequest}
121
+ disabled={state.loading || !state.requestUrl || !isJsonValid}
122
+ className="bg-primary hover:bg-primary/90 text-primary-foreground flex items-center gap-2"
123
+ >
124
+ {state.loading ? (
125
+ <>
126
+ <Loader2 className="h-4 w-4 animate-spin" />
127
+ <span className="hidden sm:inline">Sending...</span>
128
+ </>
129
+ ) : (
130
+ <>
131
+ <Send className="h-4 w-4" />
132
+ <span className="hidden sm:inline">Send Request</span>
133
+ </>
134
+ )}
135
+ </Button>
136
+ </div>
137
+ </CardContent>
138
+ </Card>
139
+
140
+ {/* cURL Command */}
141
+ {curlCommand && (
142
+ <Card>
143
+ <CardHeader>
144
+ <div className="flex items-center justify-between">
145
+ <CardTitle className="text-sm text-foreground">cURL Command</CardTitle>
146
+ <Button
147
+ variant="outline"
148
+ size="sm"
149
+ onClick={() => copyToClipboard(curlCommand)}
150
+ disabled={!curlCommand}
151
+ >
152
+ <Copy className="h-4 w-4" />
153
+ <span className="hidden sm:inline">Copy cURL</span>
154
+ </Button>
155
+ </div>
156
+ </CardHeader>
157
+ <CardContent>
158
+ <PrettyCode data={curlCommand} language="bash" />
159
+ </CardContent>
160
+ </Card>
161
+ )}
162
+ </div>
163
+ );
164
+ };