create-fluxstack 1.0.13 → 1.0.14

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 (214) hide show
  1. package/.env.example +29 -29
  2. package/app/client/README.md +69 -69
  3. package/app/client/index.html +14 -13
  4. package/app/client/src/App.tsx +157 -524
  5. package/app/client/src/components/ErrorBoundary.tsx +107 -0
  6. package/app/client/src/components/ErrorDisplay.css +365 -0
  7. package/app/client/src/components/ErrorDisplay.tsx +258 -0
  8. package/app/client/src/components/FluxStackConfig.tsx +1321 -0
  9. package/app/client/src/components/HybridLiveCounter.tsx +140 -0
  10. package/app/client/src/components/LiveClock.tsx +286 -0
  11. package/app/client/src/components/MainLayout.tsx +390 -0
  12. package/app/client/src/components/SidebarNavigation.tsx +391 -0
  13. package/app/client/src/components/StateDemo.tsx +178 -0
  14. package/app/client/src/components/SystemMonitor.tsx +1038 -0
  15. package/app/client/src/components/Teste.tsx +104 -0
  16. package/app/client/src/components/UserProfile.tsx +809 -0
  17. package/app/client/src/hooks/useAuth.ts +39 -0
  18. package/app/client/src/hooks/useNotifications.ts +56 -0
  19. package/app/client/src/lib/eden-api.ts +189 -53
  20. package/app/client/src/lib/errors.ts +340 -0
  21. package/app/client/src/lib/hooks/useErrorHandler.ts +258 -0
  22. package/app/client/src/lib/index.ts +45 -0
  23. package/app/client/src/main.tsx +3 -2
  24. package/app/client/src/pages/ApiDocs.tsx +182 -0
  25. package/app/client/src/pages/Demo.tsx +174 -0
  26. package/app/client/src/pages/HybridLive.tsx +263 -0
  27. package/app/client/src/pages/Overview.tsx +155 -0
  28. package/app/client/src/store/README.md +43 -0
  29. package/app/client/src/store/index.ts +16 -0
  30. package/app/client/src/store/slices/uiSlice.ts +151 -0
  31. package/app/client/src/store/slices/userSlice.ts +161 -0
  32. package/app/client/src/test/README.md +257 -0
  33. package/app/client/src/test/setup.ts +70 -0
  34. package/app/client/src/test/types.ts +12 -0
  35. package/app/client/src/vite-env.d.ts +1 -1
  36. package/app/client/tsconfig.app.json +44 -43
  37. package/app/client/tsconfig.json +7 -7
  38. package/app/client/tsconfig.node.json +25 -25
  39. package/app/client/zustand-setup.md +65 -0
  40. package/app/server/controllers/users.controller.ts +68 -68
  41. package/app/server/index.ts +9 -1
  42. package/app/server/live/CounterComponent.ts +191 -0
  43. package/app/server/live/FluxStackConfig.ts +529 -0
  44. package/app/server/live/LiveClockComponent.ts +214 -0
  45. package/app/server/live/SidebarNavigation.ts +156 -0
  46. package/app/server/live/SystemMonitor.ts +594 -0
  47. package/app/server/live/SystemMonitorIntegration.ts +151 -0
  48. package/app/server/live/TesteComponent.ts +87 -0
  49. package/app/server/live/UserProfileComponent.ts +135 -0
  50. package/app/server/live/register-components.ts +28 -0
  51. package/app/server/middleware/auth.ts +136 -0
  52. package/app/server/middleware/errorHandling.ts +250 -0
  53. package/app/server/middleware/index.ts +10 -0
  54. package/app/server/middleware/rateLimit.ts +193 -0
  55. package/app/server/middleware/requestLogging.ts +215 -0
  56. package/app/server/middleware/validation.ts +270 -0
  57. package/app/server/routes/index.ts +14 -2
  58. package/app/server/routes/upload.ts +92 -0
  59. package/app/server/routes/users.routes.ts +2 -9
  60. package/app/server/services/NotificationService.ts +302 -0
  61. package/app/server/services/UserService.ts +222 -0
  62. package/app/server/services/index.ts +46 -0
  63. package/core/cli/commands/plugin-deps.ts +263 -0
  64. package/core/cli/generators/README.md +339 -0
  65. package/core/cli/generators/component.ts +770 -0
  66. package/core/cli/generators/controller.ts +299 -0
  67. package/core/cli/generators/index.ts +144 -0
  68. package/core/cli/generators/interactive.ts +228 -0
  69. package/core/cli/generators/prompts.ts +83 -0
  70. package/core/cli/generators/route.ts +513 -0
  71. package/core/cli/generators/service.ts +465 -0
  72. package/core/cli/generators/template-engine.ts +154 -0
  73. package/core/cli/generators/types.ts +71 -0
  74. package/core/cli/generators/utils.ts +192 -0
  75. package/core/cli/index.ts +69 -0
  76. package/core/cli/plugin-discovery.ts +16 -85
  77. package/core/client/fluxstack.ts +17 -0
  78. package/core/client/hooks/index.ts +7 -0
  79. package/core/client/hooks/state-validator.ts +130 -0
  80. package/core/client/hooks/useAuth.ts +49 -0
  81. package/core/client/hooks/useChunkedUpload.ts +258 -0
  82. package/core/client/hooks/useHybridLiveComponent.ts +967 -0
  83. package/core/client/hooks/useWebSocket.ts +373 -0
  84. package/core/client/index.ts +47 -0
  85. package/core/client/state/createStore.ts +193 -0
  86. package/core/client/state/index.ts +15 -0
  87. package/core/config/env-dynamic.ts +1 -1
  88. package/core/config/env.ts +2 -1
  89. package/core/config/runtime-config.ts +3 -3
  90. package/core/config/schema.ts +84 -49
  91. package/core/framework/server.ts +30 -0
  92. package/core/index.ts +25 -0
  93. package/core/live/ComponentRegistry.ts +399 -0
  94. package/core/live/types.ts +164 -0
  95. package/core/plugins/built-in/live-components/commands/create-live-component.ts +1201 -0
  96. package/core/plugins/built-in/live-components/index.ts +27 -0
  97. package/core/plugins/built-in/logger/index.ts +1 -1
  98. package/core/plugins/built-in/monitoring/index.ts +1 -1
  99. package/core/plugins/built-in/static/index.ts +1 -1
  100. package/core/plugins/built-in/swagger/index.ts +1 -1
  101. package/core/plugins/built-in/vite/index.ts +1 -1
  102. package/core/plugins/dependency-manager.ts +384 -0
  103. package/core/plugins/index.ts +5 -1
  104. package/core/plugins/manager.ts +7 -3
  105. package/core/plugins/registry.ts +88 -10
  106. package/core/plugins/types.ts +11 -11
  107. package/core/server/framework.ts +43 -0
  108. package/core/server/index.ts +11 -1
  109. package/core/server/live/ComponentRegistry.ts +1017 -0
  110. package/core/server/live/FileUploadManager.ts +272 -0
  111. package/core/server/live/LiveComponentPerformanceMonitor.ts +930 -0
  112. package/core/server/live/SingleConnectionManager.ts +0 -0
  113. package/core/server/live/StateSignature.ts +644 -0
  114. package/core/server/live/WebSocketConnectionManager.ts +688 -0
  115. package/core/server/live/websocket-plugin.ts +435 -0
  116. package/core/server/middleware/errorHandling.ts +141 -0
  117. package/core/server/middleware/index.ts +16 -0
  118. package/core/server/plugins/static-files-plugin.ts +232 -0
  119. package/core/server/services/BaseService.ts +95 -0
  120. package/core/server/services/ServiceContainer.ts +144 -0
  121. package/core/server/services/index.ts +9 -0
  122. package/core/templates/create-project.ts +46 -2
  123. package/core/testing/index.ts +10 -0
  124. package/core/testing/setup.ts +74 -0
  125. package/core/types/build.ts +38 -14
  126. package/core/types/types.ts +319 -0
  127. package/core/utils/env-runtime.ts +7 -0
  128. package/core/utils/errors/handlers.ts +264 -39
  129. package/core/utils/errors/index.ts +528 -18
  130. package/core/utils/errors/middleware.ts +114 -0
  131. package/core/utils/logger/formatters.ts +222 -0
  132. package/core/utils/logger/index.ts +167 -48
  133. package/core/utils/logger/middleware.ts +253 -0
  134. package/core/utils/logger/performance.ts +384 -0
  135. package/core/utils/logger/transports.ts +365 -0
  136. package/create-fluxstack.ts +296 -296
  137. package/fluxstack.config.ts +17 -1
  138. package/package-template.json +66 -66
  139. package/package.json +31 -6
  140. package/public/README.md +16 -0
  141. package/vite.config.ts +29 -14
  142. package/.claude/settings.local.json +0 -74
  143. package/.github/workflows/ci-build-tests.yml +0 -480
  144. package/.github/workflows/dependency-management.yml +0 -324
  145. package/.github/workflows/release-validation.yml +0 -355
  146. package/.kiro/specs/fluxstack-architecture-optimization/design.md +0 -700
  147. package/.kiro/specs/fluxstack-architecture-optimization/requirements.md +0 -127
  148. package/.kiro/specs/fluxstack-architecture-optimization/tasks.md +0 -330
  149. package/CLAUDE.md +0 -200
  150. package/Dockerfile +0 -58
  151. package/Dockerfile.backend +0 -52
  152. package/Dockerfile.frontend +0 -54
  153. package/README-Docker.md +0 -85
  154. package/ai-context/00-QUICK-START.md +0 -86
  155. package/ai-context/README.md +0 -88
  156. package/ai-context/development/eden-treaty-guide.md +0 -362
  157. package/ai-context/development/patterns.md +0 -382
  158. package/ai-context/development/plugins-guide.md +0 -572
  159. package/ai-context/examples/crud-complete.md +0 -626
  160. package/ai-context/project/architecture.md +0 -399
  161. package/ai-context/project/overview.md +0 -213
  162. package/ai-context/recent-changes/eden-treaty-refactor.md +0 -281
  163. package/ai-context/recent-changes/type-inference-fix.md +0 -223
  164. package/ai-context/reference/environment-vars.md +0 -384
  165. package/ai-context/reference/troubleshooting.md +0 -407
  166. package/app/client/src/components/TestPage.tsx +0 -453
  167. package/bun.lock +0 -1063
  168. package/bunfig.toml +0 -16
  169. package/core/__tests__/integration.test.ts +0 -227
  170. package/core/build/index.ts +0 -186
  171. package/core/config/__tests__/config-loader.test.ts +0 -554
  172. package/core/config/__tests__/config-merger.test.ts +0 -657
  173. package/core/config/__tests__/env-converter.test.ts +0 -372
  174. package/core/config/__tests__/env-processor.test.ts +0 -431
  175. package/core/config/__tests__/env.test.ts +0 -452
  176. package/core/config/__tests__/integration.test.ts +0 -418
  177. package/core/config/__tests__/loader.test.ts +0 -331
  178. package/core/config/__tests__/schema.test.ts +0 -129
  179. package/core/config/__tests__/validator.test.ts +0 -318
  180. package/core/framework/__tests__/server.test.ts +0 -233
  181. package/core/plugins/__tests__/built-in.test.ts.disabled +0 -366
  182. package/core/plugins/__tests__/manager.test.ts +0 -398
  183. package/core/plugins/__tests__/monitoring.test.ts +0 -401
  184. package/core/plugins/__tests__/registry.test.ts +0 -335
  185. package/core/utils/__tests__/errors.test.ts +0 -139
  186. package/core/utils/__tests__/helpers.test.ts +0 -297
  187. package/core/utils/__tests__/logger.test.ts +0 -141
  188. package/create-test-app.ts +0 -156
  189. package/docker-compose.microservices.yml +0 -75
  190. package/docker-compose.simple.yml +0 -57
  191. package/docker-compose.yml +0 -71
  192. package/eslint.config.js +0 -23
  193. package/flux-cli.ts +0 -214
  194. package/nginx-lb.conf +0 -37
  195. package/publish.sh +0 -63
  196. package/run-clean.ts +0 -26
  197. package/run-env-tests.ts +0 -313
  198. package/tailwind.config.js +0 -34
  199. package/tests/__mocks__/api.ts +0 -56
  200. package/tests/fixtures/users.ts +0 -69
  201. package/tests/integration/api/users.routes.test.ts +0 -221
  202. package/tests/setup.ts +0 -29
  203. package/tests/unit/app/client/App-simple.test.tsx +0 -56
  204. package/tests/unit/app/client/App.test.tsx.skip +0 -237
  205. package/tests/unit/app/client/eden-api.test.ts +0 -186
  206. package/tests/unit/app/client/simple.test.tsx +0 -23
  207. package/tests/unit/app/controllers/users.controller.test.ts +0 -150
  208. package/tests/unit/core/create-project.test.ts.skip +0 -95
  209. package/tests/unit/core/framework.test.ts +0 -144
  210. package/tests/unit/core/plugins/logger.test.ts.skip +0 -268
  211. package/tests/unit/core/plugins/vite.test.ts.disabled +0 -188
  212. package/tests/utils/test-helpers.ts +0 -61
  213. package/vitest.config.ts +0 -50
  214. package/workspace.json +0 -6
@@ -0,0 +1,1201 @@
1
+ import type { CliCommand } from "../../../types";
2
+ import { promises as fs } from "fs";
3
+ import path from "path";
4
+
5
+ // Component templates for different types
6
+ const getServerTemplate = (componentName: string, type: string, room?: string) => {
7
+ const roomComment = room ? `\n // Default room: ${room}` : '';
8
+ const roomInit = room ? `\n this.room = '${room}';` : '';
9
+
10
+ switch (type) {
11
+ case 'counter':
12
+ return `// 🔥 ${componentName} - Counter Live Component
13
+ import { LiveComponent } from "@/core/types/types";
14
+
15
+ interface ${componentName}State {
16
+ count: number;
17
+ title: string;
18
+ step: number;
19
+ lastUpdated: Date;
20
+ }
21
+
22
+ export class ${componentName}Component extends LiveComponent<${componentName}State> {
23
+ constructor(initialState: ${componentName}State, ws: any, options?: { room?: string; userId?: string }) {
24
+ super({
25
+ count: 0,
26
+ title: "${componentName} Counter",
27
+ step: 1,
28
+ lastUpdated: new Date(),
29
+ ...initialState
30
+ }, ws, options);${roomComment}${roomInit}
31
+
32
+ console.log(\`🔢 \${this.constructor.name} component created: \${this.id}\`);
33
+ }
34
+
35
+ async increment(amount: number = this.state.step) {
36
+ const newCount = this.state.count + amount;
37
+
38
+ this.setState({
39
+ count: newCount,
40
+ lastUpdated: new Date()
41
+ });
42
+
43
+ // Broadcast to room for multi-user sync
44
+ if (this.room) {
45
+ this.broadcast('COUNTER_INCREMENTED', {
46
+ count: newCount,
47
+ amount,
48
+ userId: this.userId
49
+ });
50
+ }
51
+
52
+ console.log(\`🔢 Counter incremented to \${newCount} (step: \${amount})\`);
53
+ return { success: true, count: newCount };
54
+ }
55
+
56
+ async decrement(amount: number = this.state.step) {
57
+ const newCount = Math.max(0, this.state.count - amount);
58
+
59
+ this.setState({
60
+ count: newCount,
61
+ lastUpdated: new Date()
62
+ });
63
+
64
+ if (this.room) {
65
+ this.broadcast('COUNTER_DECREMENTED', {
66
+ count: newCount,
67
+ amount,
68
+ userId: this.userId
69
+ });
70
+ }
71
+
72
+ console.log(\`🔢 Counter decremented to \${newCount} (step: \${amount})\`);
73
+ return { success: true, count: newCount };
74
+ }
75
+
76
+ async reset() {
77
+ this.setState({
78
+ count: 0,
79
+ lastUpdated: new Date()
80
+ });
81
+
82
+ if (this.room) {
83
+ this.broadcast('COUNTER_RESET', { userId: this.userId });
84
+ }
85
+
86
+ console.log(\`🔢 Counter reset\`);
87
+ return { success: true, count: 0 };
88
+ }
89
+
90
+ async setStep(step: number) {
91
+ this.setState({
92
+ step: Math.max(1, step),
93
+ lastUpdated: new Date()
94
+ });
95
+
96
+ return { success: true, step };
97
+ }
98
+
99
+ async updateTitle(data: { title: string }) {
100
+ const newTitle = data.title.trim();
101
+
102
+ if (!newTitle || newTitle.length > 50) {
103
+ throw new Error('Title must be 1-50 characters');
104
+ }
105
+
106
+ this.setState({
107
+ title: newTitle,
108
+ lastUpdated: new Date()
109
+ });
110
+
111
+ return { success: true, title: newTitle };
112
+ }
113
+ }`;
114
+
115
+ case 'form':
116
+ return `// 🔥 ${componentName} - Form Live Component
117
+ import { LiveComponent } from "@/core/types/types";
118
+
119
+ interface ${componentName}State {
120
+ formData: Record<string, any>;
121
+ errors: Record<string, string>;
122
+ isSubmitting: boolean;
123
+ isValid: boolean;
124
+ lastUpdated: Date;
125
+ }
126
+
127
+ export class ${componentName}Component extends LiveComponent<${componentName}State> {
128
+ constructor(initialState: ${componentName}State, ws: any, options?: { room?: string; userId?: string }) {
129
+ super({
130
+ formData: {},
131
+ errors: {},
132
+ isSubmitting: false,
133
+ isValid: false,
134
+ lastUpdated: new Date(),
135
+ ...initialState
136
+ }, ws, options);${roomComment}${roomInit}
137
+
138
+ console.log(\`📝 \${this.constructor.name} component created: \${this.id}\`);
139
+ }
140
+
141
+ async updateField(data: { field: string; value: any }) {
142
+ const { field, value } = data;
143
+ const newFormData = { ...this.state.formData, [field]: value };
144
+ const newErrors = { ...this.state.errors };
145
+
146
+ // Clear error for this field
147
+ delete newErrors[field];
148
+
149
+ // Basic validation example
150
+ if (field === 'email' && value && !this.isValidEmail(value)) {
151
+ newErrors[field] = 'Invalid email format';
152
+ }
153
+
154
+ this.setState({
155
+ formData: newFormData,
156
+ errors: newErrors,
157
+ isValid: Object.keys(newErrors).length === 0,
158
+ lastUpdated: new Date()
159
+ });
160
+
161
+ return { success: true, field, value };
162
+ }
163
+
164
+ async submitForm() {
165
+ this.setState({ isSubmitting: true });
166
+
167
+ try {
168
+ // Simulate form submission
169
+ await this.sleep(1000);
170
+
171
+ // Validate all fields
172
+ const errors = this.validateForm(this.state.formData);
173
+
174
+ if (Object.keys(errors).length > 0) {
175
+ this.setState({
176
+ errors,
177
+ isSubmitting: false,
178
+ isValid: false
179
+ });
180
+ return { success: false, errors };
181
+ }
182
+
183
+ // Success
184
+ this.setState({
185
+ isSubmitting: false,
186
+ errors: {},
187
+ isValid: true,
188
+ lastUpdated: new Date()
189
+ });
190
+
191
+ if (this.room) {
192
+ this.broadcast('FORM_SUBMITTED', {
193
+ formData: this.state.formData,
194
+ userId: this.userId
195
+ });
196
+ }
197
+
198
+ console.log(\`📝 Form submitted successfully\`);
199
+ return { success: true, data: this.state.formData };
200
+
201
+ } catch (error: any) {
202
+ this.setState({ isSubmitting: false });
203
+ throw error;
204
+ }
205
+ }
206
+
207
+ async resetForm() {
208
+ this.setState({
209
+ formData: {},
210
+ errors: {},
211
+ isSubmitting: false,
212
+ isValid: false,
213
+ lastUpdated: new Date()
214
+ });
215
+
216
+ return { success: true };
217
+ }
218
+
219
+ private validateForm(data: Record<string, any>): Record<string, string> {
220
+ const errors: Record<string, string> = {};
221
+
222
+ // Add your validation rules here
223
+ if (!data.name || data.name.trim().length < 2) {
224
+ errors.name = 'Name must be at least 2 characters';
225
+ }
226
+
227
+ if (!data.email || !this.isValidEmail(data.email)) {
228
+ errors.email = 'Valid email is required';
229
+ }
230
+
231
+ return errors;
232
+ }
233
+
234
+ private isValidEmail(email: string): boolean {
235
+ return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);
236
+ }
237
+
238
+ private sleep(ms: number): Promise<void> {
239
+ return new Promise(resolve => setTimeout(resolve, ms));
240
+ }
241
+ }`;
242
+
243
+ case 'chat':
244
+ return `// 🔥 ${componentName} - Chat Live Component
245
+ import { LiveComponent } from "@/core/types/types";
246
+
247
+ interface Message {
248
+ id: string;
249
+ text: string;
250
+ userId: string;
251
+ username: string;
252
+ timestamp: Date;
253
+ }
254
+
255
+ interface ${componentName}State {
256
+ messages: Message[];
257
+ users: Record<string, { username: string; isOnline: boolean }>;
258
+ currentMessage: string;
259
+ isTyping: Record<string, boolean>;
260
+ lastUpdated: Date;
261
+ }
262
+
263
+ export class ${componentName}Component extends LiveComponent<${componentName}State> {
264
+ constructor(initialState: ${componentName}State, ws: any, options?: { room?: string; userId?: string }) {
265
+ super({
266
+ messages: [],
267
+ users: {},
268
+ currentMessage: "",
269
+ isTyping: {},
270
+ lastUpdated: new Date(),
271
+ ...initialState
272
+ }, ws, options);${roomComment}${roomInit}
273
+
274
+ console.log(\`💬 \${this.constructor.name} component created: \${this.id}\`);
275
+
276
+ // Add user to online users
277
+ if (this.userId) {
278
+ this.addUser(this.userId, \`User \${this.userId.slice(-4)}\`);
279
+ }
280
+ }
281
+
282
+ async sendMessage(data: { text: string; username?: string }) {
283
+ const { text, username = \`User \${this.userId?.slice(-4) || 'Anonymous'}\` } = data;
284
+
285
+ if (!text.trim()) {
286
+ throw new Error('Message cannot be empty');
287
+ }
288
+
289
+ const message: Message = {
290
+ id: \`msg-\${Date.now()}-\${Math.random().toString(36).substr(2, 9)}\`,
291
+ text: text.trim(),
292
+ userId: this.userId || 'anonymous',
293
+ username,
294
+ timestamp: new Date()
295
+ };
296
+
297
+ const newMessages = [...this.state.messages, message];
298
+
299
+ this.setState({
300
+ messages: newMessages.slice(-50), // Keep last 50 messages
301
+ currentMessage: "",
302
+ lastUpdated: new Date()
303
+ });
304
+
305
+ // Broadcast to all users in the room
306
+ if (this.room) {
307
+ this.broadcast('NEW_MESSAGE', {
308
+ message,
309
+ totalMessages: newMessages.length
310
+ });
311
+ }
312
+
313
+ console.log(\`💬 Message sent: "\${text}" by \${username}\`);
314
+ return { success: true, message };
315
+ }
316
+
317
+ async updateTyping(data: { isTyping: boolean; username?: string }) {
318
+ const { isTyping, username = \`User \${this.userId?.slice(-4)}\` } = data;
319
+ const userId = this.userId || 'anonymous';
320
+
321
+ const newTyping = { ...this.state.isTyping };
322
+
323
+ if (isTyping) {
324
+ newTyping[userId] = true;
325
+ } else {
326
+ delete newTyping[userId];
327
+ }
328
+
329
+ this.setState({
330
+ isTyping: newTyping,
331
+ lastUpdated: new Date()
332
+ });
333
+
334
+ // Broadcast typing status
335
+ if (this.room) {
336
+ this.broadcast('USER_TYPING', {
337
+ userId,
338
+ username,
339
+ isTyping
340
+ });
341
+ }
342
+
343
+ // Auto-clear typing after 3 seconds
344
+ if (isTyping) {
345
+ setTimeout(() => {
346
+ this.updateTyping({ isTyping: false, username });
347
+ }, 3000);
348
+ }
349
+
350
+ return { success: true };
351
+ }
352
+
353
+ async joinRoom(data: { username: string }) {
354
+ const { username } = data;
355
+
356
+ this.addUser(this.userId || 'anonymous', username);
357
+
358
+ if (this.room) {
359
+ this.broadcast('USER_JOINED', {
360
+ userId: this.userId,
361
+ username,
362
+ timestamp: new Date()
363
+ });
364
+ }
365
+
366
+ return { success: true, username };
367
+ }
368
+
369
+ async leaveRoom() {
370
+ const userId = this.userId || 'anonymous';
371
+ const users = { ...this.state.users };
372
+ delete users[userId];
373
+
374
+ this.setState({
375
+ users,
376
+ lastUpdated: new Date()
377
+ });
378
+
379
+ if (this.room) {
380
+ this.broadcast('USER_LEFT', {
381
+ userId,
382
+ timestamp: new Date()
383
+ });
384
+ }
385
+
386
+ return { success: true };
387
+ }
388
+
389
+ private addUser(userId: string, username: string) {
390
+ const users = {
391
+ ...this.state.users,
392
+ [userId]: { username, isOnline: true }
393
+ };
394
+
395
+ this.setState({
396
+ users,
397
+ lastUpdated: new Date()
398
+ });
399
+ }
400
+
401
+ public destroy() {
402
+ this.leaveRoom();
403
+ super.destroy();
404
+ }
405
+ }`;
406
+
407
+ default: // basic
408
+ return `// 🔥 ${componentName} - Live Component
409
+ import { LiveComponent } from "@/core/types/types";
410
+
411
+ interface ${componentName}State {
412
+ message: string;
413
+ count: number;
414
+ lastUpdated: Date;
415
+ }
416
+
417
+ export class ${componentName}Component extends LiveComponent<${componentName}State> {
418
+ constructor(initialState: ${componentName}State, ws: any, options?: { room?: string; userId?: string }) {
419
+ super({
420
+ message: "Hello from ${componentName}!",
421
+ count: 0,
422
+ lastUpdated: new Date(),
423
+ ...initialState
424
+ }, ws, options);${roomComment}${roomInit}
425
+
426
+ console.log(\`🔥 \${this.constructor.name} component created: \${this.id}\`);
427
+ }
428
+
429
+ async updateMessage(payload: { message: string }) {
430
+ const { message } = payload;
431
+
432
+ if (!message || message.trim().length === 0) {
433
+ throw new Error('Message cannot be empty');
434
+ }
435
+
436
+ this.setState({
437
+ message: message.trim(),
438
+ lastUpdated: new Date()
439
+ });
440
+
441
+ // Broadcast to room if in multi-user mode
442
+ if (this.room) {
443
+ this.broadcast('MESSAGE_UPDATED', {
444
+ message: message.trim(),
445
+ userId: this.userId
446
+ });
447
+ }
448
+
449
+ console.log(\`📝 Message updated: "\${message}"\`);
450
+ return { success: true, message: message.trim() };
451
+ }
452
+
453
+ async incrementCounter() {
454
+ const newCount = this.state.count + 1;
455
+
456
+ this.setState({
457
+ count: newCount,
458
+ lastUpdated: new Date()
459
+ });
460
+
461
+ if (this.room) {
462
+ this.broadcast('COUNTER_INCREMENTED', {
463
+ count: newCount,
464
+ userId: this.userId
465
+ });
466
+ }
467
+
468
+ return { success: true, count: newCount };
469
+ }
470
+
471
+ async resetData() {
472
+ this.setState({
473
+ message: "Hello from ${componentName}!",
474
+ count: 0,
475
+ lastUpdated: new Date()
476
+ });
477
+
478
+ return { success: true };
479
+ }
480
+
481
+ async getData() {
482
+ return {
483
+ success: true,
484
+ data: {
485
+ ...this.state,
486
+ componentId: this.id,
487
+ room: this.room,
488
+ userId: this.userId
489
+ }
490
+ };
491
+ }
492
+ }`;
493
+ }
494
+ };
495
+
496
+ const getClientTemplate = (componentName: string, type: string, room?: string) => {
497
+ const roomProps = room ? `, { room: '${room}' }` : '';
498
+
499
+ switch (type) {
500
+ case 'counter':
501
+ return `// 🔥 ${componentName} - Counter Client Component
502
+ import React from 'react';
503
+ import { useHybridLiveComponent } from 'fluxstack';
504
+
505
+ interface ${componentName}State {
506
+ count: number;
507
+ title: string;
508
+ step: number;
509
+ lastUpdated: Date;
510
+ }
511
+
512
+ const initialState: ${componentName}State = {
513
+ count: 0,
514
+ title: "${componentName} Counter",
515
+ step: 1,
516
+ lastUpdated: new Date(),
517
+ };
518
+
519
+ export function ${componentName}() {
520
+ const { state, call, connected, loading } = useHybridLiveComponent<${componentName}State>('${componentName}', initialState${roomProps});
521
+
522
+ if (!connected) {
523
+ return (
524
+ <div className="flex items-center justify-center p-8 border-2 border-dashed border-gray-300 rounded-lg">
525
+ <div className="text-center">
526
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
527
+ <p className="text-gray-600">Connecting to ${componentName}...</p>
528
+ </div>
529
+ </div>
530
+ );
531
+ }
532
+
533
+ return (
534
+ <div className="bg-white border border-gray-200 rounded-lg shadow-sm p-6 m-4 relative">
535
+ <div className="flex items-center justify-between mb-4">
536
+ <h2 className="text-2xl font-bold text-gray-800">{state.title}</h2>
537
+ <span className={
538
+ \`px-2 py-1 rounded-full text-xs font-medium \${
539
+ connected ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
540
+ }\`
541
+ }>
542
+ {connected ? '🟢 Connected' : '🔴 Disconnected'}
543
+ </span>
544
+ </div>
545
+
546
+ <div className="text-center mb-6">
547
+ <div className="text-6xl font-bold text-blue-600 mb-2">{state.count}</div>
548
+ <p className="text-gray-600">Current Count</p>
549
+ </div>
550
+
551
+ <div className="flex gap-2 justify-center mb-4">
552
+ <button
553
+ onClick={() => call('decrement')}
554
+ disabled={loading || state.count <= 0}
555
+ className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed"
556
+ >
557
+ ➖ Decrement
558
+ </button>
559
+
560
+ <button
561
+ onClick={() => call('increment')}
562
+ disabled={loading}
563
+ className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
564
+ >
565
+ ➕ Increment
566
+ </button>
567
+
568
+ <button
569
+ onClick={() => call('reset')}
570
+ disabled={loading}
571
+ className="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
572
+ >
573
+ 🔄 Reset
574
+ </button>
575
+ </div>
576
+
577
+ <div className="border-t pt-4">
578
+ <div className="flex gap-2 items-center mb-2">
579
+ <label className="text-sm font-medium text-gray-700">Step Size:</label>
580
+ <input
581
+ type="number"
582
+ min="1"
583
+ max="10"
584
+ value={state.step}
585
+ onChange={(e) => call('setStep', parseInt(e.target.value) || 1)}
586
+ className="w-20 px-2 py-1 border rounded"
587
+ disabled={loading}
588
+ />
589
+ </div>
590
+
591
+ <div className="flex gap-2 items-center">
592
+ <label className="text-sm font-medium text-gray-700">Title:</label>
593
+ <input
594
+ type="text"
595
+ value={state.title}
596
+ onChange={(e) => call('updateTitle', { title: e.target.value })}
597
+ className="flex-1 px-2 py-1 border rounded"
598
+ disabled={loading}
599
+ maxLength={50}
600
+ />
601
+ </div>
602
+ </div>
603
+
604
+ <div className="mt-4 text-xs text-gray-500 text-center">
605
+ Last updated: {new Date(state.lastUpdated).toLocaleTimeString()}
606
+ </div>
607
+
608
+ {loading && (
609
+ <div className="absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center rounded-lg">
610
+ <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
611
+ </div>
612
+ )}
613
+ </div>
614
+ );
615
+ }`;
616
+
617
+ case 'form':
618
+ return `// 🔥 ${componentName} - Form Client Component
619
+ import React from 'react';
620
+ import { useHybridLiveComponent } from 'fluxstack';
621
+
622
+ interface ${componentName}State {
623
+ formData: Record<string, any>;
624
+ errors: Record<string, string>;
625
+ isSubmitting: boolean;
626
+ isValid: boolean;
627
+ lastUpdated: Date;
628
+ }
629
+
630
+ const initialState: ${componentName}State = {
631
+ formData: {},
632
+ errors: {},
633
+ isSubmitting: false,
634
+ isValid: false,
635
+ lastUpdated: new Date(),
636
+ };
637
+
638
+ export function ${componentName}() {
639
+ const { state, call, connected, loading } = useHybridLiveComponent<${componentName}State>('${componentName}', initialState${roomProps});
640
+
641
+ if (!connected) {
642
+ return (
643
+ <div className="flex items-center justify-center p-8 border-2 border-dashed border-gray-300 rounded-lg">
644
+ <div className="text-center">
645
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
646
+ <p className="text-gray-600">Connecting to ${componentName}...</p>
647
+ </div>
648
+ </div>
649
+ );
650
+ }
651
+
652
+ const handleFieldChange = (field: string, value: any) => {
653
+ call('updateField', { field, value });
654
+ };
655
+
656
+ const handleSubmit = async (e: React.FormEvent) => {
657
+ e.preventDefault();
658
+ try {
659
+ const result = await call('submitForm');
660
+ if (result?.success) {
661
+ alert('Form submitted successfully!');
662
+ }
663
+ } catch (error) {
664
+ console.error('Submit error:', error);
665
+ }
666
+ };
667
+
668
+ const handleReset = () => {
669
+ call('resetForm');
670
+ };
671
+
672
+ return (
673
+ <div className="bg-white border border-gray-200 rounded-lg shadow-sm p-6 m-4 max-w-md mx-auto">
674
+ <div className="flex items-center justify-between mb-4">
675
+ <h2 className="text-2xl font-bold text-gray-800">${componentName} Form</h2>
676
+ <span className={
677
+ \`px-2 py-1 rounded-full text-xs font-medium \${
678
+ connected ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
679
+ }\`
680
+ }>
681
+ {connected ? '🟢 Connected' : '🔴 Disconnected'}
682
+ </span>
683
+ </div>
684
+
685
+ <form onSubmit={handleSubmit} className="space-y-4">
686
+ <div>
687
+ <label className="block text-sm font-medium text-gray-700 mb-1">
688
+ Name *
689
+ </label>
690
+ <input
691
+ type="text"
692
+ value={state.formData.name || ''}
693
+ onChange={(e) => handleFieldChange('name', e.target.value)}
694
+ className={\`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 \${
695
+ state.errors.name ? 'border-red-500' : 'border-gray-300'
696
+ }\`}
697
+ disabled={loading || state.isSubmitting}
698
+ placeholder="Enter your name"
699
+ />
700
+ {state.errors.name && (
701
+ <p className="text-red-500 text-xs mt-1">{state.errors.name}</p>
702
+ )}
703
+ </div>
704
+
705
+ <div>
706
+ <label className="block text-sm font-medium text-gray-700 mb-1">
707
+ Email *
708
+ </label>
709
+ <input
710
+ type="email"
711
+ value={state.formData.email || ''}
712
+ onChange={(e) => handleFieldChange('email', e.target.value)}
713
+ className={\`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 \${
714
+ state.errors.email ? 'border-red-500' : 'border-gray-300'
715
+ }\`}
716
+ disabled={loading || state.isSubmitting}
717
+ placeholder="Enter your email"
718
+ />
719
+ {state.errors.email && (
720
+ <p className="text-red-500 text-xs mt-1">{state.errors.email}</p>
721
+ )}
722
+ </div>
723
+
724
+ <div>
725
+ <label className="block text-sm font-medium text-gray-700 mb-1">
726
+ Message
727
+ </label>
728
+ <textarea
729
+ value={state.formData.message || ''}
730
+ onChange={(e) => handleFieldChange('message', e.target.value)}
731
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
732
+ disabled={loading || state.isSubmitting}
733
+ placeholder="Enter your message"
734
+ rows={3}
735
+ />
736
+ </div>
737
+
738
+ <div className="flex gap-2">
739
+ <button
740
+ type="submit"
741
+ disabled={loading || state.isSubmitting || !state.isValid}
742
+ className="flex-1 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
743
+ >
744
+ {state.isSubmitting ? (
745
+ <>
746
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
747
+ Submitting...
748
+ </>
749
+ ) : (
750
+ '📤 Submit'
751
+ )}
752
+ </button>
753
+
754
+ <button
755
+ type="button"
756
+ onClick={handleReset}
757
+ disabled={loading || state.isSubmitting}
758
+ className="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
759
+ >
760
+ 🔄 Reset
761
+ </button>
762
+ </div>
763
+ </form>
764
+
765
+ <div className="mt-4 text-xs text-gray-500 text-center">
766
+ Last updated: {new Date(state.lastUpdated).toLocaleTimeString()}
767
+ </div>
768
+ </div>
769
+ );
770
+ }`;
771
+
772
+ case 'chat':
773
+ return `// 🔥 ${componentName} - Chat Client Component
774
+ import React, { useState, useEffect, useRef } from 'react';
775
+ import { useHybridLiveComponent } from 'fluxstack';
776
+
777
+ interface Message {
778
+ id: string;
779
+ text: string;
780
+ userId: string;
781
+ username: string;
782
+ timestamp: Date;
783
+ }
784
+
785
+ interface ${componentName}State {
786
+ messages: Message[];
787
+ users: Record<string, { username: string; isOnline: boolean }>;
788
+ currentMessage: string;
789
+ isTyping: Record<string, boolean>;
790
+ lastUpdated: Date;
791
+ }
792
+
793
+ const initialState: ${componentName}State = {
794
+ messages: [],
795
+ users: {},
796
+ currentMessage: "",
797
+ isTyping: {},
798
+ lastUpdated: new Date(),
799
+ };
800
+
801
+ export function ${componentName}() {
802
+ const { state, call, connected, loading } = useHybridLiveComponent<${componentName}State>('${componentName}', initialState${roomProps});
803
+ const [username, setUsername] = useState(\`User\${Math.random().toString(36).substr(2, 4)}\`);
804
+ const [hasJoined, setHasJoined] = useState(false);
805
+ const messagesEndRef = useRef<HTMLDivElement>(null);
806
+
807
+ useEffect(() => {
808
+ if (messagesEndRef.current) {
809
+ messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
810
+ }
811
+ }, [state.messages]);
812
+
813
+ if (!connected) {
814
+ return (
815
+ <div className="flex items-center justify-center p-8 border-2 border-dashed border-gray-300 rounded-lg">
816
+ <div className="text-center">
817
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
818
+ <p className="text-gray-600">Connecting to ${componentName}...</p>
819
+ </div>
820
+ </div>
821
+ );
822
+ }
823
+
824
+ if (!hasJoined) {
825
+ return (
826
+ <div className="bg-white border border-gray-200 rounded-lg shadow-sm p-6 m-4 max-w-md mx-auto">
827
+ <h2 className="text-2xl font-bold text-gray-800 mb-4">Join ${componentName}</h2>
828
+ <div className="space-y-4">
829
+ <div>
830
+ <label className="block text-sm font-medium text-gray-700 mb-1">
831
+ Username
832
+ </label>
833
+ <input
834
+ type="text"
835
+ value={username}
836
+ onChange={(e) => setUsername(e.target.value)}
837
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
838
+ placeholder="Enter your username"
839
+ maxLength={20}
840
+ />
841
+ </div>
842
+ <button
843
+ onClick={async () => {
844
+ if (username.trim()) {
845
+ await call('joinRoom', { username: username.trim() });
846
+ setHasJoined(true);
847
+ }
848
+ }}
849
+ disabled={!username.trim() || loading}
850
+ className="w-full px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
851
+ >
852
+ 💬 Join Chat
853
+ </button>
854
+ </div>
855
+ </div>
856
+ );
857
+ }
858
+
859
+ const handleSendMessage = async (e: React.FormEvent) => {
860
+ e.preventDefault();
861
+ if (state.currentMessage.trim()) {
862
+ await call('sendMessage', { text: state.currentMessage, username });
863
+ }
864
+ };
865
+
866
+ const typingUsers = Object.keys(state.isTyping).filter(userId => state.isTyping[userId]);
867
+
868
+ return (
869
+ <div className="bg-white border border-gray-200 rounded-lg shadow-sm m-4 max-w-2xl mx-auto flex flex-col h-96">
870
+ {/* Header */}
871
+ <div className="flex items-center justify-between p-4 border-b border-gray-200">
872
+ <h2 className="text-xl font-bold text-gray-800">${componentName}</h2>
873
+ <div className="flex items-center gap-2">
874
+ <span className="text-sm text-gray-600">
875
+ {Object.keys(state.users).length} online
876
+ </span>
877
+ <span className={
878
+ \`px-2 py-1 rounded-full text-xs font-medium \${
879
+ connected ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
880
+ }\`
881
+ }>
882
+ {connected ? '🟢 Connected' : '🔴 Disconnected'}
883
+ </span>
884
+ </div>
885
+ </div>
886
+
887
+ {/* Messages */}
888
+ <div className="flex-1 overflow-y-auto p-4 space-y-2">
889
+ {state.messages.map((message) => (
890
+ <div
891
+ key={message.id}
892
+ className={\`flex \${message.username === username ? 'justify-end' : 'justify-start'}\`}
893
+ >
894
+ <div
895
+ className={\`max-w-xs px-3 py-2 rounded-lg \${
896
+ message.username === username
897
+ ? 'bg-blue-500 text-white'
898
+ : 'bg-gray-100 text-gray-800'
899
+ }\`}
900
+ >
901
+ {message.username !== username && (
902
+ <div className="text-xs font-medium mb-1">{message.username}</div>
903
+ )}
904
+ <div className="text-sm">{message.text}</div>
905
+ <div className={\`text-xs mt-1 \${
906
+ message.username === username ? 'text-blue-100' : 'text-gray-500'
907
+ }\`}>
908
+ {new Date(message.timestamp).toLocaleTimeString()}
909
+ </div>
910
+ </div>
911
+ </div>
912
+ ))}
913
+
914
+ {typingUsers.length > 0 && (
915
+ <div className="text-xs text-gray-500 italic">
916
+ {typingUsers.map(userId => state.users[userId]?.username || userId).join(', ')}
917
+ {typingUsers.length === 1 ? ' is' : ' are'} typing...
918
+ </div>
919
+ )}
920
+
921
+ <div ref={messagesEndRef} />
922
+ </div>
923
+
924
+ {/* Input */}
925
+ <form onSubmit={handleSendMessage} className="p-4 border-t border-gray-200">
926
+ <div className="flex gap-2">
927
+ <input
928
+ type="text"
929
+ value={state.currentMessage}
930
+ onChange={(e) => {
931
+ call('updateField', { field: 'currentMessage', value: e.target.value });
932
+ call('updateTyping', { isTyping: e.target.value.length > 0, username });
933
+ }}
934
+ className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
935
+ placeholder="Type a message..."
936
+ disabled={loading}
937
+ maxLength={500}
938
+ />
939
+ <button
940
+ type="submit"
941
+ disabled={!state.currentMessage.trim() || loading}
942
+ className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
943
+ >
944
+ 📤
945
+ </button>
946
+ </div>
947
+ </form>
948
+ </div>
949
+ );
950
+ }`;
951
+
952
+ default: // basic
953
+ return `// 🔥 ${componentName} - Client Component
954
+ import React from 'react';
955
+ import { useHybridLiveComponent } from 'fluxstack';
956
+
957
+ interface ${componentName}State {
958
+ message: string;
959
+ count: number;
960
+ lastUpdated: Date;
961
+ }
962
+
963
+ const initialState: ${componentName}State = {
964
+ message: "Loading...",
965
+ count: 0,
966
+ lastUpdated: new Date(),
967
+ };
968
+
969
+ export function ${componentName}() {
970
+ const { state, call, connected, loading } = useHybridLiveComponent<${componentName}State>('${componentName}', initialState${roomProps});
971
+
972
+ if (!connected) {
973
+ return (
974
+ <div className="flex items-center justify-center p-8 border-2 border-dashed border-gray-300 rounded-lg">
975
+ <div className="text-center">
976
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
977
+ <p className="text-gray-600">Connecting to ${componentName}...</p>
978
+ </div>
979
+ </div>
980
+ );
981
+ }
982
+
983
+ return (
984
+ <div className="bg-white border border-gray-200 rounded-lg shadow-sm p-6 m-4 relative">
985
+ <div className="flex items-center justify-between mb-4">
986
+ <h2 className="text-2xl font-bold text-gray-800">${componentName} Live Component</h2>
987
+ <span className={
988
+ \`px-2 py-1 rounded-full text-xs font-medium \${
989
+ connected ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
990
+ }\`
991
+ }>
992
+ {connected ? '🟢 Connected' : '🔴 Disconnected'}
993
+ </span>
994
+ </div>
995
+
996
+ <div className="space-y-4">
997
+ <div>
998
+ <p className="text-gray-600 mb-2">Server message:</p>
999
+ <p className="text-lg font-semibold text-blue-600">{state.message}</p>
1000
+ </div>
1001
+
1002
+ <div>
1003
+ <p className="text-gray-600 mb-2">Counter: <span className="font-bold text-2xl">{state.count}</span></p>
1004
+ </div>
1005
+
1006
+ <div className="flex gap-2 flex-wrap">
1007
+ <button
1008
+ onClick={() => call('updateMessage', { message: 'Hello from the client!' })}
1009
+ disabled={loading}
1010
+ className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
1011
+ >
1012
+ 📝 Update Message
1013
+ </button>
1014
+
1015
+ <button
1016
+ onClick={() => call('incrementCounter')}
1017
+ disabled={loading}
1018
+ className="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 disabled:opacity-50 disabled:cursor-not-allowed"
1019
+ >
1020
+ ➕ Increment
1021
+ </button>
1022
+
1023
+ <button
1024
+ onClick={() => call('resetData')}
1025
+ disabled={loading}
1026
+ className="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
1027
+ >
1028
+ 🔄 Reset
1029
+ </button>
1030
+
1031
+ <button
1032
+ onClick={async () => {
1033
+ const result = await call('getData');
1034
+ console.log('Component data:', result);
1035
+ alert('Data logged to console');
1036
+ }}
1037
+ disabled={loading}
1038
+ className="px-4 py-2 bg-purple-500 text-white rounded-lg hover:bg-purple-600 disabled:opacity-50 disabled:cursor-not-allowed"
1039
+ >
1040
+ 📊 Get Data
1041
+ </button>
1042
+ </div>
1043
+ </div>
1044
+
1045
+ <div className="mt-4 text-xs text-gray-500 text-center">
1046
+ Last updated: {new Date(state.lastUpdated).toLocaleTimeString()}
1047
+ </div>
1048
+
1049
+ {loading && (
1050
+ <div className="absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center rounded-lg">
1051
+ <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
1052
+ </div>
1053
+ )}
1054
+ </div>
1055
+ );
1056
+ }`;
1057
+ }
1058
+ };
1059
+
1060
+ export const createLiveComponentCommand: CliCommand = {
1061
+ name: "make:component",
1062
+ description: "Create a new Live Component with server and client files",
1063
+ category: "Live Components",
1064
+ aliases: ["make:live", "create:component", "create:live-component"],
1065
+ usage: "flux make:component <ComponentName> [options]",
1066
+ examples: [
1067
+ "flux make:component UserProfile # Basic component",
1068
+ "flux make:component TodoCounter --type=counter # Counter component",
1069
+ "flux make:component ContactForm --type=form # Form component",
1070
+ "flux make:component LiveChat --type=chat # Chat component",
1071
+ "flux make:component ServerOnly --no-client # Server-only component",
1072
+ "flux make:component MultiUser --room=lobby # Component with room support"
1073
+ ],
1074
+ arguments: [
1075
+ {
1076
+ name: "ComponentName",
1077
+ description: "The name of the component in PascalCase (e.g., UserProfile, TodoCounter)",
1078
+ required: true,
1079
+ type: "string"
1080
+ },
1081
+ ],
1082
+ options: [
1083
+ {
1084
+ name: "type",
1085
+ short: "t",
1086
+ description: "Type of component template to generate",
1087
+ type: "string",
1088
+ default: "basic",
1089
+ choices: ["basic", "counter", "form", "chat"]
1090
+ },
1091
+ {
1092
+ name: "no-client",
1093
+ description: "Generate only server component (no client file)",
1094
+ type: "boolean"
1095
+ },
1096
+ {
1097
+ name: "room",
1098
+ short: "r",
1099
+ description: "Default room name for multi-user features",
1100
+ type: "string"
1101
+ },
1102
+ {
1103
+ name: "force",
1104
+ short: "f",
1105
+ description: "Overwrite existing files if they exist",
1106
+ type: "boolean"
1107
+ }
1108
+ ],
1109
+ handler: async (args, options, context) => {
1110
+ const [componentName] = args;
1111
+ const { type = 'basic', 'no-client': noClient, room, force } = options;
1112
+
1113
+ // Validation
1114
+ if (!componentName || !/^[A-Z][a-zA-Z0-9]*$/.test(componentName)) {
1115
+ context.logger.error("❌ Invalid component name. It must be in PascalCase (e.g., UserProfile, TodoCounter).");
1116
+ context.logger.info("Examples: UserProfile, TodoCounter, ContactForm, LiveChat");
1117
+ return;
1118
+ }
1119
+
1120
+ if (!['basic', 'counter', 'form', 'chat'].includes(type)) {
1121
+ context.logger.error(`❌ Invalid component type: ${type}`);
1122
+ context.logger.info("Available types: basic, counter, form, chat");
1123
+ return;
1124
+ }
1125
+
1126
+ // File paths
1127
+ const serverFilePath = path.join(context.workingDir, "app", "server", "live", `${componentName}Component.ts`);
1128
+ const clientFilePath = path.join(context.workingDir, "app", "client", "src", "components", `${componentName}.tsx`);
1129
+
1130
+ try {
1131
+ // Check if files exist (unless force flag is used)
1132
+ if (!force) {
1133
+ const serverExists = await fs.access(serverFilePath).then(() => true).catch(() => false);
1134
+ const clientExists = !noClient && await fs.access(clientFilePath).then(() => true).catch(() => false);
1135
+
1136
+ if (serverExists || clientExists) {
1137
+ context.logger.error(`❌ Component files already exist. Use --force to overwrite.`);
1138
+ if (serverExists) context.logger.info(` Server: ${serverFilePath}`);
1139
+ if (clientExists) context.logger.info(` Client: ${clientFilePath}`);
1140
+ return;
1141
+ }
1142
+ }
1143
+
1144
+ // Ensure directories exist
1145
+ await fs.mkdir(path.dirname(serverFilePath), { recursive: true });
1146
+ if (!noClient) {
1147
+ await fs.mkdir(path.dirname(clientFilePath), { recursive: true });
1148
+ }
1149
+
1150
+ // Generate server component
1151
+ context.logger.info(`🔥 Creating server component: ${componentName}Component.ts`);
1152
+ const serverTemplate = getServerTemplate(componentName, type, room);
1153
+ await fs.writeFile(serverFilePath, serverTemplate);
1154
+
1155
+ // Generate client component (unless --no-client)
1156
+ if (!noClient) {
1157
+ context.logger.info(`⚛️ Creating client component: ${componentName}.tsx`);
1158
+ const clientTemplate = getClientTemplate(componentName, type, room);
1159
+ await fs.writeFile(clientFilePath, clientTemplate);
1160
+ }
1161
+
1162
+ // Success message
1163
+ context.logger.info(`✅ Successfully created '${componentName}' live component!`);
1164
+ context.logger.info("");
1165
+ context.logger.info("📁 Files created:");
1166
+ context.logger.info(` 🔥 Server: app/server/live/${componentName}Component.ts`);
1167
+ if (!noClient) {
1168
+ context.logger.info(` ⚛️ Client: app/client/src/components/${componentName}.tsx`);
1169
+ }
1170
+
1171
+ context.logger.info("");
1172
+ context.logger.info("🚀 Next steps:");
1173
+ context.logger.info(" 1. Start dev server: bun run dev");
1174
+ if (!noClient) {
1175
+ context.logger.info(` 2. Import component in your App.tsx:`);
1176
+ context.logger.info(` import { ${componentName} } from './components/${componentName}'`);
1177
+ context.logger.info(` 3. Add component to your JSX: <${componentName} />`);
1178
+ }
1179
+
1180
+ if (room) {
1181
+ context.logger.info(` 4. Component supports multi-user features with room: ${room}`);
1182
+ }
1183
+
1184
+ if (type !== 'basic') {
1185
+ context.logger.info(` 5. Template type '${type}' includes specialized functionality`);
1186
+ }
1187
+
1188
+ context.logger.info("");
1189
+ context.logger.info("📚 Import guide:");
1190
+ context.logger.info(" # Use the fluxstack library (recommended):");
1191
+ context.logger.info(" import { useHybridLiveComponent } from 'fluxstack';");
1192
+ context.logger.info("");
1193
+ context.logger.info(" # Alternative - Direct core import:");
1194
+ context.logger.info(" import { useHybridLiveComponent } from '@/core/client';");
1195
+
1196
+ } catch (error) {
1197
+ context.logger.error(`❌ Failed to create component files: ${error instanceof Error ? error.message : String(error)}`);
1198
+ throw error;
1199
+ }
1200
+ },
1201
+ };