create-fluxstack 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +29 -29
- package/app/client/README.md +69 -69
- package/app/client/index.html +14 -13
- package/app/client/src/App.tsx +157 -524
- package/app/client/src/components/ErrorBoundary.tsx +107 -0
- package/app/client/src/components/ErrorDisplay.css +365 -0
- package/app/client/src/components/ErrorDisplay.tsx +258 -0
- package/app/client/src/components/FluxStackConfig.tsx +1321 -0
- package/app/client/src/components/HybridLiveCounter.tsx +140 -0
- package/app/client/src/components/LiveClock.tsx +286 -0
- package/app/client/src/components/MainLayout.tsx +390 -0
- package/app/client/src/components/SidebarNavigation.tsx +391 -0
- package/app/client/src/components/StateDemo.tsx +178 -0
- package/app/client/src/components/SystemMonitor.tsx +1038 -0
- package/app/client/src/components/Teste.tsx +104 -0
- package/app/client/src/components/UserProfile.tsx +809 -0
- package/app/client/src/hooks/useAuth.ts +39 -0
- package/app/client/src/hooks/useNotifications.ts +56 -0
- package/app/client/src/lib/eden-api.ts +189 -53
- package/app/client/src/lib/errors.ts +340 -0
- package/app/client/src/lib/hooks/useErrorHandler.ts +258 -0
- package/app/client/src/lib/index.ts +45 -0
- package/app/client/src/main.tsx +3 -2
- package/app/client/src/pages/ApiDocs.tsx +182 -0
- package/app/client/src/pages/Demo.tsx +174 -0
- package/app/client/src/pages/HybridLive.tsx +263 -0
- package/app/client/src/pages/Overview.tsx +155 -0
- package/app/client/src/store/README.md +43 -0
- package/app/client/src/store/index.ts +16 -0
- package/app/client/src/store/slices/uiSlice.ts +151 -0
- package/app/client/src/store/slices/userSlice.ts +161 -0
- package/app/client/src/test/README.md +257 -0
- package/app/client/src/test/setup.ts +70 -0
- package/app/client/src/test/types.ts +12 -0
- package/app/client/src/vite-env.d.ts +1 -1
- package/app/client/tsconfig.app.json +44 -43
- package/app/client/tsconfig.json +7 -7
- package/app/client/tsconfig.node.json +25 -25
- package/app/client/zustand-setup.md +65 -0
- package/app/server/controllers/users.controller.ts +68 -68
- package/app/server/index.ts +9 -1
- package/app/server/live/CounterComponent.ts +191 -0
- package/app/server/live/FluxStackConfig.ts +529 -0
- package/app/server/live/LiveClockComponent.ts +214 -0
- package/app/server/live/SidebarNavigation.ts +156 -0
- package/app/server/live/SystemMonitor.ts +594 -0
- package/app/server/live/SystemMonitorIntegration.ts +151 -0
- package/app/server/live/TesteComponent.ts +87 -0
- package/app/server/live/UserProfileComponent.ts +135 -0
- package/app/server/live/register-components.ts +28 -0
- package/app/server/middleware/auth.ts +136 -0
- package/app/server/middleware/errorHandling.ts +250 -0
- package/app/server/middleware/index.ts +10 -0
- package/app/server/middleware/rateLimit.ts +193 -0
- package/app/server/middleware/requestLogging.ts +215 -0
- package/app/server/middleware/validation.ts +270 -0
- package/app/server/routes/index.ts +14 -2
- package/app/server/routes/upload.ts +92 -0
- package/app/server/routes/users.routes.ts +2 -9
- package/app/server/services/NotificationService.ts +302 -0
- package/app/server/services/UserService.ts +222 -0
- package/app/server/services/index.ts +46 -0
- package/core/cli/commands/plugin-deps.ts +263 -0
- package/core/cli/generators/README.md +339 -0
- package/core/cli/generators/component.ts +770 -0
- package/core/cli/generators/controller.ts +299 -0
- package/core/cli/generators/index.ts +144 -0
- package/core/cli/generators/interactive.ts +228 -0
- package/core/cli/generators/prompts.ts +83 -0
- package/core/cli/generators/route.ts +513 -0
- package/core/cli/generators/service.ts +465 -0
- package/core/cli/generators/template-engine.ts +154 -0
- package/core/cli/generators/types.ts +71 -0
- package/core/cli/generators/utils.ts +192 -0
- package/core/cli/index.ts +69 -0
- package/core/cli/plugin-discovery.ts +16 -85
- package/core/client/fluxstack.ts +17 -0
- package/core/client/hooks/index.ts +7 -0
- package/core/client/hooks/state-validator.ts +130 -0
- package/core/client/hooks/useAuth.ts +49 -0
- package/core/client/hooks/useChunkedUpload.ts +258 -0
- package/core/client/hooks/useHybridLiveComponent.ts +967 -0
- package/core/client/hooks/useWebSocket.ts +373 -0
- package/core/client/index.ts +47 -0
- package/core/client/state/createStore.ts +193 -0
- package/core/client/state/index.ts +15 -0
- package/core/config/env-dynamic.ts +1 -1
- package/core/config/env.ts +2 -1
- package/core/config/runtime-config.ts +3 -3
- package/core/config/schema.ts +84 -49
- package/core/framework/server.ts +30 -0
- package/core/index.ts +25 -0
- package/core/live/ComponentRegistry.ts +399 -0
- package/core/live/types.ts +164 -0
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +1201 -0
- package/core/plugins/built-in/live-components/index.ts +27 -0
- package/core/plugins/built-in/logger/index.ts +1 -1
- package/core/plugins/built-in/monitoring/index.ts +1 -1
- package/core/plugins/built-in/static/index.ts +1 -1
- package/core/plugins/built-in/swagger/index.ts +1 -1
- package/core/plugins/built-in/vite/index.ts +1 -1
- package/core/plugins/dependency-manager.ts +384 -0
- package/core/plugins/index.ts +5 -1
- package/core/plugins/manager.ts +7 -3
- package/core/plugins/registry.ts +88 -10
- package/core/plugins/types.ts +11 -11
- package/core/server/framework.ts +43 -0
- package/core/server/index.ts +11 -1
- package/core/server/live/ComponentRegistry.ts +1017 -0
- package/core/server/live/FileUploadManager.ts +272 -0
- package/core/server/live/LiveComponentPerformanceMonitor.ts +930 -0
- package/core/server/live/SingleConnectionManager.ts +0 -0
- package/core/server/live/StateSignature.ts +644 -0
- package/core/server/live/WebSocketConnectionManager.ts +688 -0
- package/core/server/live/websocket-plugin.ts +435 -0
- package/core/server/middleware/errorHandling.ts +141 -0
- package/core/server/middleware/index.ts +16 -0
- package/core/server/plugins/static-files-plugin.ts +232 -0
- package/core/server/services/BaseService.ts +95 -0
- package/core/server/services/ServiceContainer.ts +144 -0
- package/core/server/services/index.ts +9 -0
- package/core/templates/create-project.ts +196 -33
- package/core/testing/index.ts +10 -0
- package/core/testing/setup.ts +74 -0
- package/core/types/build.ts +38 -14
- package/core/types/types.ts +319 -0
- package/core/utils/env-runtime.ts +7 -0
- package/core/utils/errors/handlers.ts +264 -39
- package/core/utils/errors/index.ts +528 -18
- package/core/utils/errors/middleware.ts +114 -0
- package/core/utils/logger/formatters.ts +222 -0
- package/core/utils/logger/index.ts +167 -48
- package/core/utils/logger/middleware.ts +253 -0
- package/core/utils/logger/performance.ts +384 -0
- package/core/utils/logger/transports.ts +365 -0
- package/create-fluxstack.ts +296 -296
- package/fluxstack.config.ts +17 -1
- package/package-template.json +66 -66
- package/package.json +31 -6
- package/public/README.md +16 -0
- package/vite.config.ts +29 -14
- package/.claude/settings.local.json +0 -74
- package/.github/workflows/ci-build-tests.yml +0 -480
- package/.github/workflows/dependency-management.yml +0 -324
- package/.github/workflows/release-validation.yml +0 -355
- package/.kiro/specs/fluxstack-architecture-optimization/design.md +0 -700
- package/.kiro/specs/fluxstack-architecture-optimization/requirements.md +0 -127
- package/.kiro/specs/fluxstack-architecture-optimization/tasks.md +0 -330
- package/CLAUDE.md +0 -200
- package/Dockerfile +0 -58
- package/Dockerfile.backend +0 -52
- package/Dockerfile.frontend +0 -54
- package/README-Docker.md +0 -85
- package/ai-context/00-QUICK-START.md +0 -86
- package/ai-context/README.md +0 -88
- package/ai-context/development/eden-treaty-guide.md +0 -362
- package/ai-context/development/patterns.md +0 -382
- package/ai-context/development/plugins-guide.md +0 -572
- package/ai-context/examples/crud-complete.md +0 -626
- package/ai-context/project/architecture.md +0 -399
- package/ai-context/project/overview.md +0 -213
- package/ai-context/recent-changes/eden-treaty-refactor.md +0 -281
- package/ai-context/recent-changes/type-inference-fix.md +0 -223
- package/ai-context/reference/environment-vars.md +0 -384
- package/ai-context/reference/troubleshooting.md +0 -407
- package/app/client/src/components/TestPage.tsx +0 -453
- package/bun.lock +0 -1063
- package/bunfig.toml +0 -16
- package/core/__tests__/integration.test.ts +0 -227
- package/core/build/index.ts +0 -186
- package/core/config/__tests__/config-loader.test.ts +0 -554
- package/core/config/__tests__/config-merger.test.ts +0 -657
- package/core/config/__tests__/env-converter.test.ts +0 -372
- package/core/config/__tests__/env-processor.test.ts +0 -431
- package/core/config/__tests__/env.test.ts +0 -452
- package/core/config/__tests__/integration.test.ts +0 -418
- package/core/config/__tests__/loader.test.ts +0 -331
- package/core/config/__tests__/schema.test.ts +0 -129
- package/core/config/__tests__/validator.test.ts +0 -318
- package/core/framework/__tests__/server.test.ts +0 -233
- package/core/plugins/__tests__/built-in.test.ts.disabled +0 -366
- package/core/plugins/__tests__/manager.test.ts +0 -398
- package/core/plugins/__tests__/monitoring.test.ts +0 -401
- package/core/plugins/__tests__/registry.test.ts +0 -335
- package/core/utils/__tests__/errors.test.ts +0 -139
- package/core/utils/__tests__/helpers.test.ts +0 -297
- package/core/utils/__tests__/logger.test.ts +0 -141
- package/create-test-app.ts +0 -156
- package/docker-compose.microservices.yml +0 -75
- package/docker-compose.simple.yml +0 -57
- package/docker-compose.yml +0 -71
- package/eslint.config.js +0 -23
- package/flux-cli.ts +0 -214
- package/nginx-lb.conf +0 -37
- package/publish.sh +0 -63
- package/run-clean.ts +0 -26
- package/run-env-tests.ts +0 -313
- package/tailwind.config.js +0 -34
- package/tests/__mocks__/api.ts +0 -56
- package/tests/fixtures/users.ts +0 -69
- package/tests/integration/api/users.routes.test.ts +0 -221
- package/tests/setup.ts +0 -29
- package/tests/unit/app/client/App-simple.test.tsx +0 -56
- package/tests/unit/app/client/App.test.tsx.skip +0 -237
- package/tests/unit/app/client/eden-api.test.ts +0 -186
- package/tests/unit/app/client/simple.test.tsx +0 -23
- package/tests/unit/app/controllers/users.controller.test.ts +0 -150
- package/tests/unit/core/create-project.test.ts.skip +0 -95
- package/tests/unit/core/framework.test.ts +0 -144
- package/tests/unit/core/plugins/logger.test.ts.skip +0 -268
- package/tests/unit/core/plugins/vite.test.ts.disabled +0 -188
- package/tests/utils/test-helpers.ts +0 -61
- package/vitest.config.ts +0 -50
- 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
|
+
};
|