create-fluxstack 1.9.1 → 1.12.0
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/.dockerignore +1 -2
- package/Dockerfile +8 -8
- package/LIVE_COMPONENTS_REVIEW.md +781 -0
- package/LLMD/INDEX.md +64 -0
- package/LLMD/MAINTENANCE.md +197 -0
- package/LLMD/MIGRATION.md +156 -0
- package/LLMD/config/.gitkeep +1 -0
- package/LLMD/config/declarative-system.md +268 -0
- package/LLMD/config/environment-vars.md +327 -0
- package/LLMD/config/runtime-reload.md +401 -0
- package/LLMD/core/.gitkeep +1 -0
- package/LLMD/core/build-system.md +599 -0
- package/LLMD/core/framework-lifecycle.md +229 -0
- package/LLMD/core/plugin-system.md +451 -0
- package/LLMD/patterns/.gitkeep +1 -0
- package/LLMD/patterns/anti-patterns.md +297 -0
- package/LLMD/patterns/project-structure.md +264 -0
- package/LLMD/patterns/type-safety.md +440 -0
- package/LLMD/reference/.gitkeep +1 -0
- package/LLMD/reference/cli-commands.md +250 -0
- package/LLMD/reference/plugin-hooks.md +357 -0
- package/LLMD/reference/routing.md +39 -0
- package/LLMD/reference/troubleshooting.md +364 -0
- package/LLMD/resources/.gitkeep +1 -0
- package/LLMD/resources/controllers.md +465 -0
- package/LLMD/resources/live-components.md +703 -0
- package/LLMD/resources/live-rooms.md +482 -0
- package/LLMD/resources/live-upload.md +130 -0
- package/LLMD/resources/plugins-external.md +617 -0
- package/LLMD/resources/routes-eden.md +254 -0
- package/README.md +37 -17
- package/app/client/index.html +0 -1
- package/app/client/src/App.tsx +109 -156
- package/app/client/src/components/AppLayout.tsx +68 -0
- package/app/client/src/components/BackButton.tsx +13 -0
- package/app/client/src/components/DemoPage.tsx +20 -0
- package/app/client/src/components/LiveUploadWidget.tsx +204 -0
- package/app/client/src/lib/eden-api.ts +85 -65
- package/app/client/src/live/ChatDemo.tsx +107 -0
- package/app/client/src/live/CounterDemo.tsx +206 -0
- package/app/client/src/live/FormDemo.tsx +119 -0
- package/app/client/src/live/RoomChatDemo.tsx +242 -0
- package/app/client/src/live/UploadDemo.tsx +21 -0
- package/app/client/src/main.tsx +13 -10
- package/app/client/src/pages/ApiTestPage.tsx +108 -0
- package/app/client/src/pages/HomePage.tsx +76 -0
- package/app/client/src/vite-env.d.ts +1 -1
- package/app/server/app.ts +1 -4
- package/app/server/controllers/users.controller.ts +36 -44
- package/app/server/index.ts +24 -107
- package/app/server/live/LiveChat.ts +77 -0
- package/app/server/live/LiveCounter.ts +67 -0
- package/app/server/live/LiveForm.ts +63 -0
- package/app/server/live/LiveLocalCounter.ts +32 -0
- package/app/server/live/LiveRoomChat.ts +285 -0
- package/app/server/live/LiveUpload.ts +81 -0
- package/app/server/live/register-components.ts +19 -19
- package/app/server/routes/index.ts +3 -1
- package/app/server/routes/room.routes.ts +117 -0
- package/app/server/routes/users.routes.ts +35 -27
- package/app/shared/types/index.ts +14 -2
- package/config/app.config.ts +2 -62
- package/config/client.config.ts +2 -95
- package/config/database.config.ts +2 -99
- package/config/fluxstack.config.ts +25 -45
- package/config/index.ts +57 -38
- package/config/monitoring.config.ts +2 -114
- package/config/plugins.config.ts +2 -80
- package/config/server.config.ts +2 -68
- package/config/services.config.ts +2 -130
- package/config/system/app.config.ts +29 -0
- package/config/system/build.config.ts +49 -0
- package/config/system/client.config.ts +68 -0
- package/config/system/database.config.ts +17 -0
- package/config/system/fluxstack.config.ts +114 -0
- package/config/{logger.config.ts → system/logger.config.ts} +3 -1
- package/config/system/monitoring.config.ts +114 -0
- package/config/system/plugins.config.ts +84 -0
- package/config/{runtime.config.ts → system/runtime.config.ts} +1 -1
- package/config/system/server.config.ts +68 -0
- package/config/system/services.config.ts +46 -0
- package/config/{system.config.ts → system/system.config.ts} +1 -1
- package/core/build/bundler.ts +4 -1
- package/core/build/flux-plugins-generator.ts +325 -325
- package/core/build/index.ts +159 -27
- package/core/build/live-components-generator.ts +70 -3
- package/core/build/optimizer.ts +235 -235
- package/core/cli/command-registry.ts +6 -4
- package/core/cli/commands/build.ts +79 -0
- package/core/cli/commands/create.ts +54 -0
- package/core/cli/commands/dev.ts +101 -0
- package/core/cli/commands/help.ts +34 -0
- package/core/cli/commands/index.ts +34 -0
- package/core/cli/commands/make-plugin.ts +90 -0
- package/core/cli/commands/plugin-add.ts +197 -0
- package/core/cli/commands/plugin-deps.ts +2 -2
- package/core/cli/commands/plugin-list.ts +208 -0
- package/core/cli/commands/plugin-remove.ts +170 -0
- package/core/cli/generators/component.ts +769 -769
- package/core/cli/generators/controller.ts +1 -1
- package/core/cli/generators/index.ts +146 -146
- package/core/cli/generators/interactive.ts +227 -227
- package/core/cli/generators/plugin.ts +2 -2
- package/core/cli/generators/prompts.ts +82 -82
- package/core/cli/generators/route.ts +6 -6
- package/core/cli/generators/service.ts +2 -2
- package/core/cli/generators/template-engine.ts +4 -3
- package/core/cli/generators/types.ts +2 -2
- package/core/cli/generators/utils.ts +191 -191
- package/core/cli/index.ts +115 -558
- package/core/cli/plugin-discovery.ts +2 -2
- package/core/client/LiveComponentsProvider.tsx +63 -17
- package/core/client/api/eden.ts +183 -0
- package/core/client/api/index.ts +11 -0
- package/core/client/components/Live.tsx +104 -0
- package/core/client/fluxstack.ts +1 -9
- package/core/client/hooks/AdaptiveChunkSizer.ts +215 -0
- package/core/client/hooks/state-validator.ts +1 -1
- package/core/client/hooks/useAuth.ts +48 -48
- package/core/client/hooks/useChunkedUpload.ts +170 -69
- package/core/client/hooks/useLiveChunkedUpload.ts +87 -0
- package/core/client/hooks/useLiveComponent.ts +800 -0
- package/core/client/hooks/useLiveUpload.ts +71 -0
- package/core/client/hooks/useRoom.ts +409 -0
- package/core/client/hooks/useRoomProxy.ts +382 -0
- package/core/client/index.ts +18 -51
- package/core/client/standalone-entry.ts +8 -0
- package/core/client/standalone.ts +74 -53
- package/core/client/state/createStore.ts +192 -192
- package/core/client/state/index.ts +14 -14
- package/core/config/index.ts +70 -291
- package/core/config/schema.ts +42 -723
- package/core/framework/client.ts +131 -131
- package/core/framework/index.ts +7 -7
- package/core/framework/server.ts +227 -47
- package/core/framework/types.ts +2 -2
- package/core/index.ts +23 -4
- package/core/live/ComponentRegistry.ts +7 -3
- package/core/live/types.ts +77 -0
- package/core/plugins/built-in/index.ts +134 -131
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +242 -1074
- package/core/plugins/built-in/live-components/index.ts +1 -1
- package/core/plugins/built-in/monitoring/index.ts +111 -47
- package/core/plugins/built-in/static/index.ts +1 -1
- package/core/plugins/built-in/swagger/index.ts +68 -265
- package/core/plugins/built-in/vite/index.ts +94 -306
- package/core/plugins/built-in/vite/vite-dev.ts +82 -0
- package/core/plugins/config.ts +9 -7
- package/core/plugins/dependency-manager.ts +31 -1
- package/core/plugins/discovery.ts +19 -7
- package/core/plugins/executor.ts +2 -2
- package/core/plugins/index.ts +203 -203
- package/core/plugins/manager.ts +27 -39
- package/core/plugins/module-resolver.ts +19 -8
- package/core/plugins/registry.ts +309 -21
- package/core/plugins/types.ts +106 -55
- package/core/server/framework.ts +66 -43
- package/core/server/index.ts +15 -16
- package/core/server/live/ComponentRegistry.ts +91 -75
- package/core/server/live/FileUploadManager.ts +41 -31
- package/core/server/live/LiveComponentPerformanceMonitor.ts +1 -1
- package/core/server/live/LiveRoomManager.ts +261 -0
- package/core/server/live/RoomEventBus.ts +234 -0
- package/core/server/live/RoomStateManager.ts +172 -0
- package/core/server/live/StateSignature.ts +643 -643
- package/core/server/live/WebSocketConnectionManager.ts +30 -19
- package/core/server/live/auto-generated-components.ts +41 -26
- package/core/server/live/index.ts +14 -0
- package/core/server/live/websocket-plugin.ts +233 -72
- package/core/server/middleware/elysia-helpers.ts +7 -2
- package/core/server/middleware/errorHandling.ts +1 -1
- package/core/server/middleware/index.ts +31 -31
- package/core/server/plugins/database.ts +180 -180
- package/core/server/plugins/static-files-plugin.ts +69 -260
- package/core/server/plugins/swagger.ts +33 -33
- package/core/server/rooms/RoomBroadcaster.ts +357 -0
- package/core/server/rooms/RoomSystem.ts +463 -0
- package/core/server/rooms/index.ts +13 -0
- package/core/server/services/BaseService.ts +1 -1
- package/core/server/services/ServiceContainer.ts +1 -1
- package/core/server/services/index.ts +8 -8
- package/core/templates/create-project.ts +12 -12
- package/core/testing/index.ts +9 -9
- package/core/testing/setup.ts +73 -73
- package/core/types/api.ts +168 -168
- package/core/types/build.ts +219 -218
- package/core/types/config.ts +56 -26
- package/core/types/index.ts +4 -4
- package/core/types/plugin.ts +107 -99
- package/core/types/types.ts +490 -14
- package/core/utils/build-logger.ts +324 -324
- package/core/utils/config-schema.ts +480 -480
- package/core/utils/env.ts +2 -8
- package/core/utils/errors/codes.ts +114 -114
- package/core/utils/errors/handlers.ts +36 -1
- package/core/utils/errors/index.ts +49 -5
- package/core/utils/errors/middleware.ts +113 -113
- package/core/utils/helpers.ts +6 -16
- package/core/utils/index.ts +17 -17
- package/core/utils/logger/colors.ts +114 -114
- package/core/utils/logger/config.ts +13 -9
- package/core/utils/logger/formatter.ts +82 -82
- package/core/utils/logger/group-logger.ts +101 -101
- package/core/utils/logger/index.ts +6 -1
- package/core/utils/logger/stack-trace.ts +3 -1
- package/core/utils/logger/startup-banner.ts +82 -66
- package/core/utils/logger/winston-logger.ts +152 -152
- package/core/utils/monitoring/index.ts +211 -211
- package/core/utils/sync-version.ts +66 -66
- package/core/utils/version.ts +1 -1
- package/create-fluxstack.ts +8 -7
- package/eslint.config.js +23 -23
- package/package.json +14 -15
- package/plugins/crypto-auth/cli/make-protected-route.command.ts +1 -1
- package/plugins/crypto-auth/client/CryptoAuthClient.ts +302 -302
- package/plugins/crypto-auth/client/components/index.ts +11 -11
- package/plugins/crypto-auth/client/index.ts +11 -11
- package/plugins/crypto-auth/config/index.ts +1 -1
- package/plugins/crypto-auth/index.ts +4 -4
- package/plugins/crypto-auth/package.json +65 -65
- package/plugins/crypto-auth/server/AuthMiddleware.ts +1 -1
- package/plugins/crypto-auth/server/CryptoAuthService.ts +185 -185
- package/plugins/crypto-auth/server/index.ts +21 -21
- package/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts +3 -3
- package/plugins/crypto-auth/server/middlewares/cryptoAuthOptional.ts +1 -1
- package/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts +2 -2
- package/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts +2 -2
- package/plugins/crypto-auth/server/middlewares/helpers.ts +1 -1
- package/plugins/crypto-auth/server/middlewares/index.ts +22 -22
- package/plugins/crypto-auth/server/middlewares.ts +19 -19
- package/tsconfig.api-strict.json +16 -0
- package/tsconfig.json +10 -14
- package/{app/client/tsconfig.node.json → tsconfig.node.json} +1 -1
- package/types/global.d.ts +29 -29
- package/types/vitest.d.ts +8 -8
- package/vite.config.ts +38 -62
- package/vitest.config.live.ts +10 -9
- package/vitest.config.ts +29 -17
- package/workspace.json +5 -5
- package/app/client/README.md +0 -69
- package/app/client/SIMPLIFICATION.md +0 -140
- package/app/client/frontend-only.ts +0 -12
- package/app/client/tsconfig.app.json +0 -44
- package/app/client/tsconfig.json +0 -7
- package/app/client/zustand-setup.md +0 -65
- package/app/server/backend-only.ts +0 -18
- package/app/server/live/LiveClockComponent.ts +0 -215
- package/app/server/routes/env-test.ts +0 -110
- package/core/client/hooks/index.ts +0 -7
- package/core/client/hooks/useHybridLiveComponent.ts +0 -631
- package/core/client/hooks/useWebSocket.ts +0 -373
- package/core/config/env.ts +0 -546
- package/core/config/loader.ts +0 -522
- package/core/config/runtime-config.ts +0 -327
- package/core/config/validator.ts +0 -540
- package/core/server/backend-entry.ts +0 -51
- package/core/server/standalone.ts +0 -106
- package/core/utils/regenerate-files.ts +0 -69
- package/fluxstack.config.ts +0 -354
|
@@ -1,1201 +1,369 @@
|
|
|
1
|
-
import type { CliCommand } from "
|
|
1
|
+
import type { CliCommand } from "@core/plugins/types";
|
|
2
2
|
import { promises as fs } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
// ===== SERVER TEMPLATES =====
|
|
6
|
+
|
|
7
|
+
const getServerTemplate = (name: string, type: string, room?: string, hasClient = true) => {
|
|
8
|
+
// Se room for especificado, precisa de constructor para definir this.room
|
|
9
|
+
const needsConstructor = !!room;
|
|
10
|
+
const constructorBlock = needsConstructor ? `
|
|
11
|
+
constructor(initialState: Partial<typeof ${name}.defaultState> = {}, ws: FluxStackWebSocket, options?: { room?: string; userId?: string }) {
|
|
12
|
+
super(initialState, ws, options)
|
|
13
|
+
this.room = '${room}'
|
|
14
|
+
}
|
|
15
|
+
` : '';
|
|
16
|
+
|
|
17
|
+
const clientLink = hasClient ? `
|
|
18
|
+
// Componente Cliente (Ctrl+Click para navegar)
|
|
19
|
+
import type { ${name}Demo as _Client } from '@client/src/live/${name}'
|
|
20
|
+
` : '';
|
|
9
21
|
|
|
10
22
|
switch (type) {
|
|
11
23
|
case 'counter':
|
|
12
|
-
return `//
|
|
13
|
-
import { LiveComponent } from
|
|
24
|
+
return `// ${name} - Contador
|
|
25
|
+
import { LiveComponent${needsConstructor ? ', type FluxStackWebSocket' : ''} } from '@core/types/types'
|
|
26
|
+
${clientLink}
|
|
14
27
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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}\`);
|
|
28
|
+
export class ${name} extends LiveComponent<typeof ${name}.defaultState> {
|
|
29
|
+
static componentName = '${name}'
|
|
30
|
+
static defaultState = {
|
|
31
|
+
count: 0
|
|
33
32
|
}
|
|
34
|
-
|
|
35
|
-
async increment(
|
|
36
|
-
|
|
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 };
|
|
33
|
+
${constructorBlock}
|
|
34
|
+
async increment() {
|
|
35
|
+
this.state.count++
|
|
36
|
+
return { success: true, count: this.state.count }
|
|
54
37
|
}
|
|
55
38
|
|
|
56
|
-
async decrement(
|
|
57
|
-
|
|
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 };
|
|
39
|
+
async decrement() {
|
|
40
|
+
this.state.count--
|
|
41
|
+
return { success: true, count: this.state.count }
|
|
74
42
|
}
|
|
75
43
|
|
|
76
44
|
async reset() {
|
|
77
|
-
this.
|
|
78
|
-
|
|
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 };
|
|
45
|
+
this.state.count = 0
|
|
46
|
+
return { success: true }
|
|
97
47
|
}
|
|
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
48
|
}
|
|
49
|
+
`;
|
|
126
50
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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';
|
|
51
|
+
case 'form':
|
|
52
|
+
return `// ${name} - Formulário
|
|
53
|
+
import { LiveComponent${needsConstructor ? ', type FluxStackWebSocket' : ''} } from '@core/types/types'
|
|
54
|
+
${clientLink}
|
|
55
|
+
|
|
56
|
+
export class ${name} extends LiveComponent<typeof ${name}.defaultState> {
|
|
57
|
+
static componentName = '${name}'
|
|
58
|
+
static defaultState = {
|
|
59
|
+
name: '',
|
|
60
|
+
email: '',
|
|
61
|
+
message: '',
|
|
62
|
+
submitted: false,
|
|
63
|
+
submittedAt: null as string | null
|
|
64
|
+
}
|
|
65
|
+
${constructorBlock}
|
|
66
|
+
async submit() {
|
|
67
|
+
if (!this.state.name?.trim() || !this.state.email?.trim()) {
|
|
68
|
+
throw new Error('Nome e email são obrigatórios')
|
|
229
69
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
private isValidEmail(email: string): boolean {
|
|
235
|
-
return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);
|
|
70
|
+
this.setState({ submitted: true, submittedAt: new Date().toISOString() })
|
|
71
|
+
console.log(\`📝 Form submitted:\`, { name: this.state.name, email: this.state.email })
|
|
72
|
+
return { success: true, data: this.state }
|
|
236
73
|
}
|
|
237
74
|
|
|
238
|
-
|
|
239
|
-
|
|
75
|
+
async reset() {
|
|
76
|
+
this.setState({ ...${name}.defaultState })
|
|
77
|
+
return { success: true }
|
|
240
78
|
}
|
|
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
79
|
}
|
|
80
|
+
`;
|
|
254
81
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
});
|
|
82
|
+
case 'chat':
|
|
83
|
+
return `// ${name} - Chat
|
|
84
|
+
import { LiveComponent${needsConstructor ? ', type FluxStackWebSocket' : ''} } from '@core/types/types'
|
|
85
|
+
${clientLink}
|
|
86
|
+
|
|
87
|
+
export class ${name} extends LiveComponent<typeof ${name}.defaultState> {
|
|
88
|
+
static componentName = '${name}'
|
|
89
|
+
static defaultState = {
|
|
90
|
+
messages: [] as Array<{ id: string; text: string; username: string; timestamp: string }>,
|
|
91
|
+
username: '',
|
|
92
|
+
currentMessage: ''
|
|
93
|
+
}
|
|
94
|
+
${constructorBlock}
|
|
95
|
+
async sendMessage(payload: { text: string }) {
|
|
96
|
+
if (!payload.text?.trim()) throw new Error('Message cannot be empty')
|
|
97
|
+
const message = {
|
|
98
|
+
id: \`msg-\${Date.now()}-\${Math.random().toString(36).slice(2, 9)}\`,
|
|
99
|
+
text: payload.text.trim(),
|
|
100
|
+
username: this.state.username || 'Anonymous',
|
|
101
|
+
timestamp: new Date().toISOString()
|
|
364
102
|
}
|
|
365
|
-
|
|
366
|
-
|
|
103
|
+
this.setState({ messages: [...this.state.messages.slice(-49), message], currentMessage: '' })
|
|
104
|
+
if (this.room) this.broadcast('NEW_MESSAGE', { message })
|
|
105
|
+
return { success: true, message }
|
|
367
106
|
}
|
|
368
107
|
|
|
369
|
-
async
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
});
|
|
108
|
+
async setUsername(payload: { username: string }) {
|
|
109
|
+
if (!payload.username?.trim() || payload.username.length > 20) {
|
|
110
|
+
throw new Error('Username must be 1-20 characters')
|
|
384
111
|
}
|
|
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();
|
|
112
|
+
this.setState({ username: payload.username.trim() })
|
|
113
|
+
return { success: true }
|
|
404
114
|
}
|
|
405
|
-
}
|
|
115
|
+
}
|
|
116
|
+
`;
|
|
406
117
|
|
|
407
118
|
default: // basic
|
|
408
|
-
return `//
|
|
409
|
-
import { LiveComponent } from
|
|
119
|
+
return `// ${name} - Live Component
|
|
120
|
+
import { LiveComponent${needsConstructor ? ', type FluxStackWebSocket' : ''} } from '@core/types/types'
|
|
121
|
+
${clientLink}
|
|
410
122
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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}\`);
|
|
123
|
+
export class ${name} extends LiveComponent<typeof ${name}.defaultState> {
|
|
124
|
+
static componentName = '${name}'
|
|
125
|
+
static defaultState = {
|
|
126
|
+
message: 'Hello from ${name}!',
|
|
127
|
+
count: 0
|
|
427
128
|
}
|
|
428
|
-
|
|
129
|
+
${constructorBlock}
|
|
429
130
|
async updateMessage(payload: { message: string }) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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 };
|
|
131
|
+
if (!payload.message?.trim()) throw new Error('Message cannot be empty')
|
|
132
|
+
this.state.message = payload.message.trim()
|
|
133
|
+
return { success: true }
|
|
469
134
|
}
|
|
470
135
|
|
|
471
|
-
async
|
|
472
|
-
this.
|
|
473
|
-
|
|
474
|
-
count: 0,
|
|
475
|
-
lastUpdated: new Date()
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
return { success: true };
|
|
136
|
+
async increment() {
|
|
137
|
+
this.state.count++
|
|
138
|
+
return { success: true, count: this.state.count }
|
|
479
139
|
}
|
|
480
140
|
|
|
481
|
-
async
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
data: {
|
|
485
|
-
...this.state,
|
|
486
|
-
componentId: this.id,
|
|
487
|
-
room: this.room,
|
|
488
|
-
userId: this.userId
|
|
489
|
-
}
|
|
490
|
-
};
|
|
141
|
+
async reset() {
|
|
142
|
+
this.setState({ ...${name}.defaultState })
|
|
143
|
+
return { success: true }
|
|
491
144
|
}
|
|
492
|
-
}
|
|
145
|
+
}
|
|
146
|
+
`;
|
|
493
147
|
}
|
|
494
148
|
};
|
|
495
149
|
|
|
496
|
-
|
|
497
|
-
const roomProps = room ? `, { room: '${room}' }` : '';
|
|
150
|
+
// ===== CLIENT TEMPLATES =====
|
|
498
151
|
|
|
152
|
+
const getClientTemplate = (name: string, type: string) => {
|
|
499
153
|
switch (type) {
|
|
500
154
|
case 'counter':
|
|
501
|
-
return `// 🔥 ${
|
|
502
|
-
import
|
|
503
|
-
import {
|
|
504
|
-
|
|
505
|
-
interface ${componentName}State {
|
|
506
|
-
count: number;
|
|
507
|
-
title: string;
|
|
508
|
-
step: number;
|
|
509
|
-
lastUpdated: Date;
|
|
510
|
-
}
|
|
155
|
+
return `// 🔥 ${name}
|
|
156
|
+
import { Live } from '@/core/client'
|
|
157
|
+
import { ${name} } from '@server/live/${name}'
|
|
511
158
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
lastUpdated: new Date(),
|
|
517
|
-
};
|
|
518
|
-
|
|
519
|
-
export function ${componentName}() {
|
|
520
|
-
const { state, call, connected, loading } = useHybridLiveComponent<${componentName}State>('${componentName}', initialState${roomProps});
|
|
159
|
+
export function ${name}Demo() {
|
|
160
|
+
const counter = Live.use(${name}, {
|
|
161
|
+
initialState: ${name}.defaultState
|
|
162
|
+
})
|
|
521
163
|
|
|
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
|
-
}
|
|
164
|
+
if (!counter.$connected) return <div className="p-8 text-center text-gray-500">Conectando...</div>
|
|
532
165
|
|
|
533
166
|
return (
|
|
534
|
-
<div className="bg-white
|
|
535
|
-
<
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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>
|
|
167
|
+
<div className="p-6 bg-white rounded-xl shadow-sm border">
|
|
168
|
+
<h2 className="text-xl font-bold mb-4">${name}</h2>
|
|
169
|
+
<div className="text-5xl font-bold text-blue-600 text-center mb-6">{counter.count}</div>
|
|
170
|
+
<div className="flex gap-2 justify-center">
|
|
171
|
+
<button onClick={() => counter.decrement()} disabled={counter.count <= 0} className="px-4 py-2 bg-red-500 text-white rounded-lg disabled:opacity-50">-</button>
|
|
172
|
+
<button onClick={() => counter.increment()} className="px-4 py-2 bg-blue-500 text-white rounded-lg">+</button>
|
|
173
|
+
<button onClick={() => counter.reset()} className="px-4 py-2 bg-gray-500 text-white rounded-lg">Reset</button>
|
|
575
174
|
</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
175
|
</div>
|
|
614
|
-
)
|
|
615
|
-
}
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
`;
|
|
616
179
|
|
|
617
180
|
case 'form':
|
|
618
|
-
return `// 🔥 ${
|
|
619
|
-
import
|
|
620
|
-
import {
|
|
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
|
-
}
|
|
181
|
+
return `// 🔥 ${name}
|
|
182
|
+
import { Live } from '@/core/client'
|
|
183
|
+
import { ${name} } from '@server/live/${name}'
|
|
629
184
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
isValid: false,
|
|
635
|
-
lastUpdated: new Date(),
|
|
636
|
-
};
|
|
185
|
+
export function ${name}Demo() {
|
|
186
|
+
const form = Live.use(${name}, {
|
|
187
|
+
initialState: ${name}.defaultState
|
|
188
|
+
})
|
|
637
189
|
|
|
638
|
-
|
|
639
|
-
const { state, call, connected, loading } = useHybridLiveComponent<${componentName}State>('${componentName}', initialState${roomProps});
|
|
190
|
+
if (!form.$connected) return <div className="p-8 text-center text-gray-500">Conectando...</div>
|
|
640
191
|
|
|
641
|
-
if (
|
|
192
|
+
if (form.submitted) {
|
|
642
193
|
return (
|
|
643
|
-
<div className="
|
|
644
|
-
<
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
</div>
|
|
194
|
+
<div className="p-6 bg-green-50 rounded-xl text-center">
|
|
195
|
+
<h2 className="text-xl font-bold text-green-700 mb-2">Enviado!</h2>
|
|
196
|
+
<p className="text-gray-600">Obrigado, {form.name}!</p>
|
|
197
|
+
<button onClick={() => form.reset()} className="mt-4 px-4 py-2 bg-green-500 text-white rounded-lg">Novo</button>
|
|
648
198
|
</div>
|
|
649
|
-
)
|
|
199
|
+
)
|
|
650
200
|
}
|
|
651
201
|
|
|
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
202
|
return (
|
|
673
|
-
<div className="bg-white
|
|
674
|
-
<
|
|
675
|
-
|
|
676
|
-
<
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
203
|
+
<div className="p-6 bg-white rounded-xl shadow-sm border">
|
|
204
|
+
<h2 className="text-xl font-bold mb-4">${name}</h2>
|
|
205
|
+
<div className="space-y-4">
|
|
206
|
+
<input {...form.$field('name', { syncOn: 'blur' })} placeholder="Nome" className="w-full px-3 py-2 border rounded-lg" />
|
|
207
|
+
<input {...form.$field('email', { syncOn: 'change', debounce: 500 })} type="email" placeholder="Email" className="w-full px-3 py-2 border rounded-lg" />
|
|
208
|
+
<textarea {...form.$field('message', { syncOn: 'blur' })} placeholder="Mensagem" rows={3} className="w-full px-3 py-2 border rounded-lg" />
|
|
738
209
|
<div className="flex gap-2">
|
|
739
|
-
<button
|
|
740
|
-
|
|
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>
|
|
210
|
+
<button onClick={async () => { try { await form.$sync(); await form.submit() } catch (e: any) { alert(e.message) }}} className="flex-1 px-4 py-2 bg-blue-500 text-white rounded-lg">Enviar</button>
|
|
211
|
+
<button onClick={() => form.reset()} className="px-4 py-2 bg-gray-500 text-white rounded-lg">Limpar</button>
|
|
762
212
|
</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
213
|
</div>
|
|
768
214
|
</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;
|
|
215
|
+
)
|
|
783
216
|
}
|
|
217
|
+
`;
|
|
784
218
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
lastUpdated: Date;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
const initialState: ${componentName}State = {
|
|
794
|
-
messages: [],
|
|
795
|
-
users: {},
|
|
796
|
-
currentMessage: "",
|
|
797
|
-
isTyping: {},
|
|
798
|
-
lastUpdated: new Date(),
|
|
799
|
-
};
|
|
219
|
+
case 'chat':
|
|
220
|
+
return `// 🔥 ${name}
|
|
221
|
+
import { useRef, useEffect } from 'react'
|
|
222
|
+
import { Live } from '@/core/client'
|
|
223
|
+
import { ${name} } from '@server/live/${name}'
|
|
800
224
|
|
|
801
|
-
export function ${
|
|
802
|
-
const
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
225
|
+
export function ${name}Demo() {
|
|
226
|
+
const chat = Live.use(${name}, {
|
|
227
|
+
initialState: ${name}.defaultState
|
|
228
|
+
})
|
|
229
|
+
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
806
230
|
|
|
807
|
-
useEffect(() => {
|
|
808
|
-
if (messagesEndRef.current) {
|
|
809
|
-
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
|
810
|
-
}
|
|
811
|
-
}, [state.messages]);
|
|
231
|
+
useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [chat.messages])
|
|
812
232
|
|
|
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
|
-
}
|
|
233
|
+
if (!chat.$connected) return <div className="p-8 text-center text-gray-500">Conectando...</div>
|
|
823
234
|
|
|
824
|
-
if (!
|
|
235
|
+
if (!chat.username) {
|
|
825
236
|
return (
|
|
826
|
-
<div className="bg-white
|
|
827
|
-
<h2 className="text-
|
|
828
|
-
<
|
|
829
|
-
|
|
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>
|
|
237
|
+
<div className="p-6 bg-white rounded-xl shadow-sm border max-w-md mx-auto">
|
|
238
|
+
<h2 className="text-xl font-bold mb-4">Entrar no ${name}</h2>
|
|
239
|
+
<input {...chat.$field('username', { syncOn: 'blur' })} placeholder="Seu nome" className="w-full px-3 py-2 border rounded-lg mb-4" maxLength={20} />
|
|
240
|
+
<button onClick={async () => { await chat.$sync(); if (chat.username.trim()) await chat.setUsername({ username: chat.username }) }} className="w-full px-4 py-2 bg-blue-500 text-white rounded-lg">Entrar</button>
|
|
855
241
|
</div>
|
|
856
|
-
)
|
|
242
|
+
)
|
|
857
243
|
}
|
|
858
244
|
|
|
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
245
|
return (
|
|
869
|
-
<div className="
|
|
870
|
-
{
|
|
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 */}
|
|
246
|
+
<div className="flex flex-col h-96 bg-white rounded-xl shadow-sm border">
|
|
247
|
+
<div className="p-4 border-b flex justify-between"><h2 className="font-bold">${name}</h2><span className="text-sm text-gray-500">{chat.username}</span></div>
|
|
888
248
|
<div className="flex-1 overflow-y-auto p-4 space-y-2">
|
|
889
|
-
{
|
|
890
|
-
<div
|
|
891
|
-
|
|
892
|
-
|
|
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>
|
|
249
|
+
{chat.messages.map((msg) => (
|
|
250
|
+
<div key={msg.id} className={\`flex \${msg.username === chat.username ? 'justify-end' : 'justify-start'}\`}>
|
|
251
|
+
<div className={\`max-w-xs px-3 py-2 rounded-lg \${msg.username === chat.username ? 'bg-blue-500 text-white' : 'bg-gray-100'}\`}>
|
|
252
|
+
{msg.username !== chat.username && <div className="text-xs font-medium mb-1">{msg.username}</div>}
|
|
253
|
+
<div>{msg.text}</div>
|
|
910
254
|
</div>
|
|
911
255
|
</div>
|
|
912
256
|
))}
|
|
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
257
|
<div ref={messagesEndRef} />
|
|
922
258
|
</div>
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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>
|
|
259
|
+
<form onSubmit={async (e) => { e.preventDefault(); if (chat.currentMessage.trim()) await chat.sendMessage({ text: chat.currentMessage }) }} className="p-4 border-t flex gap-2">
|
|
260
|
+
<input {...chat.$field('currentMessage', { syncOn: 'change', debounce: 100 })} placeholder="Mensagem..." className="flex-1 px-3 py-2 border rounded-lg" />
|
|
261
|
+
<button type="submit" className="px-4 py-2 bg-blue-500 text-white rounded-lg">Enviar</button>
|
|
947
262
|
</form>
|
|
948
263
|
</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;
|
|
264
|
+
)
|
|
961
265
|
}
|
|
266
|
+
`;
|
|
962
267
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
};
|
|
268
|
+
default: // basic
|
|
269
|
+
return `// 🔥 ${name}
|
|
270
|
+
import { Live } from '@/core/client'
|
|
271
|
+
import { ${name} } from '@server/live/${name}'
|
|
968
272
|
|
|
969
|
-
export function ${
|
|
970
|
-
const
|
|
273
|
+
export function ${name}Demo() {
|
|
274
|
+
const component = Live.use(${name}, {
|
|
275
|
+
initialState: ${name}.defaultState
|
|
276
|
+
})
|
|
971
277
|
|
|
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
|
-
}
|
|
278
|
+
if (!component.$connected) return <div className="p-8 text-center text-gray-500">Conectando...</div>
|
|
982
279
|
|
|
983
280
|
return (
|
|
984
|
-
<div className="bg-white
|
|
985
|
-
<
|
|
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
|
-
|
|
281
|
+
<div className="p-6 bg-white rounded-xl shadow-sm border">
|
|
282
|
+
<h2 className="text-xl font-bold mb-4">${name}</h2>
|
|
996
283
|
<div className="space-y-4">
|
|
997
|
-
<
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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>
|
|
284
|
+
<p className="text-lg text-blue-600">{component.message}</p>
|
|
285
|
+
<p className="text-gray-600">Count: <span className="font-bold text-2xl">{component.count}</span></p>
|
|
286
|
+
<div className="flex gap-2">
|
|
287
|
+
<button onClick={() => component.updateMessage({ message: 'Hello!' })} className="px-4 py-2 bg-blue-500 text-white rounded-lg">Update</button>
|
|
288
|
+
<button onClick={() => component.increment()} className="px-4 py-2 bg-green-500 text-white rounded-lg">+1</button>
|
|
289
|
+
<button onClick={() => component.reset()} className="px-4 py-2 bg-gray-500 text-white rounded-lg">Reset</button>
|
|
1042
290
|
</div>
|
|
1043
291
|
</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
292
|
</div>
|
|
1055
|
-
)
|
|
1056
|
-
}
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
`;
|
|
1057
296
|
}
|
|
1058
297
|
};
|
|
1059
298
|
|
|
1060
299
|
export const createLiveComponentCommand: CliCommand = {
|
|
1061
|
-
name: "make:
|
|
1062
|
-
description: "Create a new Live Component
|
|
300
|
+
name: "make:live",
|
|
301
|
+
description: "Create a new Live Component",
|
|
1063
302
|
category: "Live Components",
|
|
1064
|
-
aliases: ["make:
|
|
1065
|
-
usage: "flux make:
|
|
303
|
+
aliases: ["make:component", "create:live"],
|
|
304
|
+
usage: "flux make:live <Name> [options]",
|
|
1066
305
|
examples: [
|
|
1067
|
-
"flux make:
|
|
1068
|
-
"flux make:
|
|
1069
|
-
"flux make:
|
|
1070
|
-
"flux make:
|
|
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
|
-
},
|
|
306
|
+
"flux make:live Counter --type=counter",
|
|
307
|
+
"flux make:live ContactForm --type=form",
|
|
308
|
+
"flux make:live Chat --type=chat",
|
|
309
|
+
"flux make:live MyComponent"
|
|
1081
310
|
],
|
|
311
|
+
arguments: [{ name: "Name", description: "Component name in PascalCase", required: true, type: "string" }],
|
|
1082
312
|
options: [
|
|
1083
|
-
{
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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
|
-
}
|
|
313
|
+
{ name: "type", short: "t", description: "Template type", type: "string", default: "basic", choices: ["basic", "counter", "form", "chat"] },
|
|
314
|
+
{ name: "no-client", description: "Server only", type: "boolean" },
|
|
315
|
+
{ name: "room", short: "r", description: "Default room", type: "string" },
|
|
316
|
+
{ name: "force", short: "f", description: "Overwrite", type: "boolean" }
|
|
1108
317
|
],
|
|
1109
318
|
handler: async (args, options, context) => {
|
|
1110
|
-
const [
|
|
319
|
+
const [name] = args;
|
|
1111
320
|
const { type = 'basic', 'no-client': noClient, room, force } = options;
|
|
1112
321
|
|
|
1113
|
-
|
|
1114
|
-
|
|
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");
|
|
322
|
+
if (!name || !/^[A-Z][a-zA-Z0-9]*$/.test(name)) {
|
|
323
|
+
context.logger.error("❌ Nome inválido. Use PascalCase (ex: MeuComponente)");
|
|
1117
324
|
return;
|
|
1118
325
|
}
|
|
1119
326
|
|
|
1120
|
-
|
|
1121
|
-
|
|
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`);
|
|
327
|
+
const serverPath = path.join(context.workingDir, "app", "server", "live", `${name}.ts`);
|
|
328
|
+
const clientPath = path.join(context.workingDir, "app", "client", "src", "live", `${name}.tsx`);
|
|
1129
329
|
|
|
1130
330
|
try {
|
|
1131
|
-
// Check if files exist (unless force flag is used)
|
|
1132
331
|
if (!force) {
|
|
1133
|
-
const serverExists = await fs.access(
|
|
1134
|
-
const clientExists = !noClient && await fs.access(
|
|
1135
|
-
|
|
332
|
+
const serverExists = await fs.access(serverPath).then(() => true).catch(() => false);
|
|
333
|
+
const clientExists = !noClient && await fs.access(clientPath).then(() => true).catch(() => false);
|
|
1136
334
|
if (serverExists || clientExists) {
|
|
1137
|
-
context.logger.error(
|
|
1138
|
-
if (serverExists) context.logger.info(` Server: ${serverFilePath}`);
|
|
1139
|
-
if (clientExists) context.logger.info(` Client: ${clientFilePath}`);
|
|
335
|
+
context.logger.error("❌ Arquivos existem. Use --force para sobrescrever.");
|
|
1140
336
|
return;
|
|
1141
337
|
}
|
|
1142
338
|
}
|
|
1143
339
|
|
|
1144
|
-
|
|
1145
|
-
await fs.mkdir(path.dirname(
|
|
1146
|
-
if (!noClient) {
|
|
1147
|
-
await fs.mkdir(path.dirname(clientFilePath), { recursive: true });
|
|
1148
|
-
}
|
|
340
|
+
await fs.mkdir(path.dirname(serverPath), { recursive: true });
|
|
341
|
+
if (!noClient) await fs.mkdir(path.dirname(clientPath), { recursive: true });
|
|
1149
342
|
|
|
1150
|
-
|
|
1151
|
-
context.logger.info(`🔥 Creating server component: ${componentName}Component.ts`);
|
|
1152
|
-
const serverTemplate = getServerTemplate(componentName, type, room);
|
|
1153
|
-
await fs.writeFile(serverFilePath, serverTemplate);
|
|
343
|
+
context.logger.info(`🔥 Criando ${name}...`);
|
|
1154
344
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
context.logger.info(`⚛️ Creating client component: ${componentName}.tsx`);
|
|
1158
|
-
const clientTemplate = getClientTemplate(componentName, type, room);
|
|
1159
|
-
await fs.writeFile(clientFilePath, clientTemplate);
|
|
1160
|
-
}
|
|
345
|
+
await fs.writeFile(serverPath, getServerTemplate(name, type, room, !noClient));
|
|
346
|
+
context.logger.info(` ✅ Server: app/server/live/${name}.ts`);
|
|
1161
347
|
|
|
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
348
|
if (!noClient) {
|
|
1175
|
-
|
|
1176
|
-
context.logger.info(`
|
|
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`);
|
|
349
|
+
await fs.writeFile(clientPath, getClientTemplate(name, type));
|
|
350
|
+
context.logger.info(` ✅ Client: app/client/src/live/${name}.tsx`);
|
|
1186
351
|
}
|
|
1187
352
|
|
|
1188
353
|
context.logger.info("");
|
|
1189
|
-
context.logger.info("
|
|
1190
|
-
context.logger.info(
|
|
1191
|
-
context.logger.info(
|
|
354
|
+
context.logger.info("🚀 Uso:");
|
|
355
|
+
context.logger.info(` import { ${name}Demo } from './live/${name}'`);
|
|
356
|
+
context.logger.info(` <${name}Demo />`);
|
|
1192
357
|
context.logger.info("");
|
|
1193
|
-
context.logger.info("
|
|
1194
|
-
context.logger.info(
|
|
358
|
+
context.logger.info("📖 API:");
|
|
359
|
+
context.logger.info(` const x = Live.use(${name}) // usa defaultState do backend`);
|
|
360
|
+
context.logger.info(` const x = Live.use(${name}, { ... }) // com override parcial`);
|
|
361
|
+
context.logger.info(` x.property // estado`);
|
|
362
|
+
context.logger.info(` x.action() // ação`);
|
|
363
|
+
context.logger.info(` x.$connected // status`);
|
|
1195
364
|
|
|
1196
365
|
} catch (error) {
|
|
1197
|
-
context.logger.error(`❌
|
|
1198
|
-
throw error;
|
|
366
|
+
context.logger.error(`❌ Erro: ${error instanceof Error ? error.message : String(error)}`);
|
|
1199
367
|
}
|
|
1200
368
|
},
|
|
1201
|
-
};
|
|
369
|
+
};
|