create-fluxstack 1.12.0 → 1.12.1

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.
@@ -1,16 +1,9 @@
1
- // 🔥 RoomChatDemo - Demo do chat com múltiplas salas
2
- //
3
- // Demonstra o uso do sistema $room para:
4
- // - Entrar/sair de múltiplas salas
5
- // - Enviar mensagens para sala ativa
6
- // - Ver quem está digitando
7
- // - Trocar entre salas
1
+ // 🔥 RoomChatDemo - Chat multi-salas simplificado
8
2
 
9
3
  import { useState, useEffect, useRef, useMemo } from 'react'
10
4
  import { Live } from '@/core/client'
11
5
  import { LiveRoomChat } from '@server/live/LiveRoomChat'
12
6
 
13
- // Salas disponíveis
14
7
  const AVAILABLE_ROOMS = [
15
8
  { id: 'geral', name: '💬 Geral' },
16
9
  { id: 'tech', name: '💻 Tecnologia' },
@@ -22,32 +15,23 @@ export function RoomChatDemo() {
22
15
  const [text, setText] = useState('')
23
16
  const messagesEndRef = useRef<HTMLDivElement>(null)
24
17
 
25
- // Username aleatório
26
18
  const defaultUsername = useMemo(() => {
27
- const adjectives = ['Happy', 'Cool', 'Fast', 'Smart', 'Brave']
28
- const nouns = ['Panda', 'Tiger', 'Eagle', 'Wolf', 'Bear']
29
- const adj = adjectives[Math.floor(Math.random() * adjectives.length)]
30
- const noun = nouns[Math.floor(Math.random() * nouns.length)]
31
- const num = Math.floor(Math.random() * 100)
32
- return `${adj}${noun}${num}`
19
+ const adj = ['Happy', 'Cool', 'Fast', 'Smart', 'Brave'][Math.floor(Math.random() * 5)]
20
+ const noun = ['Panda', 'Tiger', 'Eagle', 'Wolf', 'Bear'][Math.floor(Math.random() * 5)]
21
+ return `${adj}${noun}${Math.floor(Math.random() * 100)}`
33
22
  }, [])
34
23
 
35
- // Live component - estado sincronizado automaticamente
36
24
  const chat = Live.use(LiveRoomChat, {
37
25
  initialState: { ...LiveRoomChat.defaultState, username: defaultUsername }
38
26
  })
39
27
 
40
- // Mensagens e typing vêm diretamente do estado sincronizado
41
28
  const activeRoom = chat.$state.activeRoom
42
29
  const activeMessages = activeRoom ? (chat.$state.messages[activeRoom] || []) : []
43
- const activeTyping = activeRoom ? (chat.$state.typingUsers[activeRoom] || []) : []
44
30
 
45
- // Auto scroll quando mensagens mudam
46
31
  useEffect(() => {
47
32
  messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
48
33
  }, [activeMessages.length])
49
34
 
50
- // Handlers
51
35
  const handleJoinRoom = async (roomId: string, roomName: string) => {
52
36
  if (chat.$rooms.includes(roomId)) {
53
37
  await chat.switchRoom({ roomId })
@@ -56,25 +40,16 @@ export function RoomChatDemo() {
56
40
  }
57
41
  }
58
42
 
59
- const handleLeaveRoom = async (roomId: string) => {
60
- await chat.leaveRoom({ roomId })
61
- }
62
-
63
43
  const handleSendMessage = async () => {
64
- if (!text.trim() || !chat.$state.activeRoom) return
44
+ if (!text.trim() || !activeRoom) return
65
45
  await chat.sendMessage({ text })
66
46
  setText('')
67
47
  }
68
48
 
69
- const handleTyping = () => {
70
- chat.startTyping({})
71
- }
72
-
73
49
  return (
74
50
  <div className="flex h-[600px] bg-gray-900 rounded-2xl overflow-hidden border border-white/10">
75
- {/* Sidebar - Lista de Salas */}
51
+ {/* Sidebar */}
76
52
  <div className="w-64 bg-gray-800/50 border-r border-white/10 flex flex-col">
77
- {/* Header */}
78
53
  <div className="p-4 border-b border-white/10">
79
54
  <h2 className="text-lg font-bold text-white mb-2">💬 Room Chat</h2>
80
55
  <div className="flex items-center gap-2">
@@ -83,57 +58,38 @@ export function RoomChatDemo() {
83
58
  </div>
84
59
  </div>
85
60
 
86
- {/* Salas Disponíveis */}
87
61
  <div className="flex-1 overflow-auto p-2">
88
62
  <p className="text-xs text-gray-500 px-2 py-1">SALAS</p>
89
63
  {AVAILABLE_ROOMS.map(room => {
90
64
  const isJoined = chat.$rooms.includes(room.id)
91
- const isActive = chat.$state.activeRoom === room.id
92
- const unreadCount = 0 // TODO: implementar contagem
65
+ const isActive = activeRoom === room.id
93
66
 
94
67
  return (
95
68
  <div
96
69
  key={room.id}
97
70
  onClick={() => handleJoinRoom(room.id, room.name)}
98
71
  className={`
99
- flex items-center justify-between px-3 py-2 rounded-lg cursor-pointer mb-1
100
- transition-all group
101
- ${isActive
102
- ? 'bg-purple-500/20 text-purple-300'
103
- : isJoined
104
- ? 'bg-white/5 text-gray-300 hover:bg-white/10'
105
- : 'text-gray-500 hover:bg-white/5 hover:text-gray-400'
106
- }
72
+ flex items-center justify-between px-3 py-2 rounded-lg cursor-pointer mb-1 transition-all group
73
+ ${isActive ? 'bg-purple-500/20 text-purple-300' : isJoined ? 'bg-white/5 text-gray-300 hover:bg-white/10' : 'text-gray-500 hover:bg-white/5'}
107
74
  `}
108
75
  >
109
76
  <span className="flex items-center gap-2">
110
77
  {room.name}
111
- {isJoined && (
112
- <span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
113
- )}
78
+ {isJoined && <span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />}
114
79
  </span>
115
-
116
80
  {isJoined && !isActive && (
117
81
  <button
118
- onClick={(e) => {
119
- e.stopPropagation()
120
- handleLeaveRoom(room.id)
121
- }}
82
+ onClick={(e) => { e.stopPropagation(); chat.leaveRoom({ roomId: room.id }) }}
122
83
  className="opacity-0 group-hover:opacity-100 text-red-400 hover:text-red-300 text-xs"
123
- >
124
-
125
- </button>
84
+ >✕</button>
126
85
  )}
127
86
  </div>
128
87
  )
129
88
  })}
130
89
  </div>
131
90
 
132
- {/* Salas ativas */}
133
91
  <div className="p-3 border-t border-white/10">
134
- <p className="text-xs text-gray-500 mb-1">
135
- Em {chat.$rooms.length} sala(s)
136
- </p>
92
+ <p className="text-xs text-gray-500">Em {chat.$rooms.length} sala(s)</p>
137
93
  </div>
138
94
  </div>
139
95
 
@@ -141,25 +97,19 @@ export function RoomChatDemo() {
141
97
  <div className="flex-1 flex flex-col">
142
98
  {activeRoom ? (
143
99
  <>
144
- {/* Header da sala */}
145
100
  <div className="px-4 py-3 border-b border-white/10 flex items-center justify-between">
146
101
  <div>
147
102
  <h3 className="text-white font-semibold">
148
103
  {chat.$state.rooms.find(r => r.id === activeRoom)?.name || activeRoom}
149
104
  </h3>
150
- <p className="text-xs text-gray-500">
151
- {activeMessages.length} mensagens
152
- </p>
105
+ <p className="text-xs text-gray-500">{activeMessages.length} mensagens</p>
153
106
  </div>
154
107
  <button
155
- onClick={() => handleLeaveRoom(activeRoom)}
156
- className="px-3 py-1 text-sm bg-red-500/20 text-red-300 rounded-lg hover:bg-red-500/30 transition-all"
157
- >
158
- Sair
159
- </button>
108
+ onClick={() => chat.leaveRoom({ roomId: activeRoom })}
109
+ className="px-3 py-1 text-sm bg-red-500/20 text-red-300 rounded-lg hover:bg-red-500/30"
110
+ >Sair</button>
160
111
  </div>
161
112
 
162
- {/* Mensagens */}
163
113
  <div className="flex-1 overflow-auto p-4 space-y-3">
164
114
  {activeMessages.length === 0 ? (
165
115
  <div className="text-center text-gray-500 py-8">
@@ -168,63 +118,32 @@ export function RoomChatDemo() {
168
118
  </div>
169
119
  ) : (
170
120
  activeMessages.map(msg => (
171
- <div
172
- key={msg.id}
173
- className={`flex flex-col ${msg.user === chat.$state.username ? 'items-end' : 'items-start'}`}
174
- >
175
- <div
176
- className={`
177
- max-w-[80%] rounded-2xl px-4 py-2
178
- ${msg.user === chat.$state.username
179
- ? 'bg-purple-500/30 text-purple-100'
180
- : 'bg-white/10 text-gray-200'
181
- }
182
- `}
183
- >
121
+ <div key={msg.id} className={`flex flex-col ${msg.user === chat.$state.username ? 'items-end' : 'items-start'}`}>
122
+ <div className={`max-w-[80%] rounded-2xl px-4 py-2 ${msg.user === chat.$state.username ? 'bg-purple-500/30 text-purple-100' : 'bg-white/10 text-gray-200'}`}>
184
123
  <p className="text-xs text-gray-400 mb-1">{msg.user}</p>
185
124
  <p>{msg.text}</p>
186
125
  </div>
187
- <span className="text-xs text-gray-600 mt-1">
188
- {new Date(msg.timestamp).toLocaleTimeString()}
189
- </span>
126
+ <span className="text-xs text-gray-600 mt-1">{new Date(msg.timestamp).toLocaleTimeString()}</span>
190
127
  </div>
191
128
  ))
192
129
  )}
193
130
  <div ref={messagesEndRef} />
194
131
  </div>
195
132
 
196
- {/* Typing indicator */}
197
- {activeTyping.length > 0 && (
198
- <div className="px-4 py-1 text-xs text-gray-500">
199
- {activeTyping.filter(u => u !== chat.$state.username).join(', ')} está digitando...
200
- </div>
201
- )}
202
-
203
- {/* Input */}
204
133
  <div className="p-4 border-t border-white/10">
205
134
  <div className="flex gap-2">
206
135
  <input
207
136
  value={text}
208
- onChange={(e) => {
209
- setText(e.target.value)
210
- handleTyping()
211
- }}
212
- onKeyDown={(e) => {
213
- if (e.key === 'Enter' && !e.shiftKey) {
214
- e.preventDefault()
215
- handleSendMessage()
216
- }
217
- }}
137
+ onChange={(e) => setText(e.target.value)}
138
+ onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSendMessage() } }}
218
139
  placeholder="Digite uma mensagem..."
219
140
  className="flex-1 px-4 py-2 rounded-xl bg-white/10 border border-white/20 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500/50"
220
141
  />
221
142
  <button
222
143
  onClick={handleSendMessage}
223
144
  disabled={!text.trim()}
224
- className="px-6 py-2 rounded-xl bg-purple-500/30 text-purple-200 hover:bg-purple-500/40 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
225
- >
226
- Enviar
227
- </button>
145
+ className="px-6 py-2 rounded-xl bg-purple-500/30 text-purple-200 hover:bg-purple-500/40 disabled:opacity-50"
146
+ >Enviar</button>
228
147
  </div>
229
148
  </div>
230
149
  </>
@@ -1,11 +1,10 @@
1
- // LiveRoomChat - Chat usando o sistema de salas $room
1
+ // LiveRoomChat - Chat multi-salas simplificado
2
2
 
3
3
  import { LiveComponent, type FluxStackWebSocket } from '@core/types/types'
4
4
 
5
5
  // Componente Cliente (Ctrl+Click para navegar)
6
6
  import type { RoomChatDemo as _Client } from '@client/src/live/RoomChatDemo'
7
7
 
8
- // Tipos
9
8
  export interface ChatMessage {
10
9
  id: string
11
10
  user: string
@@ -13,23 +12,17 @@ export interface ChatMessage {
13
12
  timestamp: number
14
13
  }
15
14
 
16
- export interface RoomInfo {
17
- id: string
18
- name: string
19
- }
20
-
21
15
  export class LiveRoomChat extends LiveComponent<typeof LiveRoomChat.defaultState> {
22
16
  static componentName = 'LiveRoomChat'
23
17
  static defaultState = {
24
18
  username: '',
25
19
  activeRoom: null as string | null,
26
- rooms: [] as RoomInfo[],
27
- messages: {} as Record<string, ChatMessage[]>,
28
- typingUsers: {} as Record<string, string[]>
20
+ rooms: [] as { id: string; name: string }[],
21
+ messages: {} as Record<string, ChatMessage[]>
29
22
  }
30
- protected roomType = 'room-chat'
31
23
 
32
- private typingTimers = new Map<string, NodeJS.Timeout>()
24
+ // Listeners por sala para evitar duplicação
25
+ private roomListeners = new Map<string, (() => void)[]>()
33
26
 
34
27
  constructor(
35
28
  initialState: Partial<typeof LiveRoomChat.defaultState> = {},
@@ -39,141 +32,68 @@ export class LiveRoomChat extends LiveComponent<typeof LiveRoomChat.defaultState
39
32
  super(initialState, ws, options)
40
33
  }
41
34
 
42
- // ===== Gerenciamento de Salas =====
43
-
44
- /**
45
- * Entrar em uma sala de chat
46
- */
47
35
  async joinRoom(payload: { roomId: string; roomName?: string }) {
48
36
  const { roomId, roomName } = payload
49
37
 
50
- // Entrar na sala
51
- this.$room(roomId).join()
52
-
53
- // Escutar mensagens novas de outros usuários
54
- this.$room(roomId).on('message:new', (msg: ChatMessage) => {
55
- this.addMessageToState(roomId, msg)
56
- })
38
+ // está na sala? Apenas ativar
39
+ if (this.roomListeners.has(roomId)) {
40
+ this.state.activeRoom = roomId
41
+ return { success: true, roomId }
42
+ }
57
43
 
58
- // Escutar usuários digitando
59
- this.$room(roomId).on('user:typing', (data: { user: string; typing: boolean }) => {
60
- this.updateTypingUsers(roomId, data.user, data.typing)
44
+ // Entrar e escutar mensagens
45
+ this.$room(roomId).join()
46
+ const unsub = this.$room(roomId).on('message:new', (msg: ChatMessage) => {
47
+ const msgs = this.state.messages[roomId] || []
48
+ this.setState({
49
+ messages: { ...this.state.messages, [roomId]: [...msgs, msg].slice(-100) }
50
+ })
61
51
  })
52
+ this.roomListeners.set(roomId, [unsub])
62
53
 
63
- // Atualizar lista de salas e inicializar mensagens
64
- const rooms = [
65
- ...this.state.rooms.filter(r => r.id !== roomId),
66
- { id: roomId, name: roomName || roomId }
67
- ]
68
-
54
+ // Atualizar estado
69
55
  this.setState({
70
- rooms,
71
56
  activeRoom: roomId,
72
- messages: {
73
- ...this.state.messages,
74
- [roomId]: this.state.messages[roomId] || []
75
- },
76
- typingUsers: {
77
- ...this.state.typingUsers,
78
- [roomId]: []
79
- }
80
- })
81
-
82
- return {
83
- success: true,
84
- roomId,
85
- rooms: this.$rooms
86
- }
87
- }
88
-
89
- private addMessageToState(roomId: string, msg: ChatMessage) {
90
- const currentMessages = this.state.messages[roomId] || []
91
- const newMessages = [...currentMessages, msg].slice(-100)
92
-
93
- this.setState({
94
- messages: {
95
- ...this.state.messages,
96
- [roomId]: newMessages
97
- }
57
+ rooms: [...this.state.rooms.filter(r => r.id !== roomId), { id: roomId, name: roomName || roomId }],
58
+ messages: { ...this.state.messages, [roomId]: this.state.messages[roomId] || [] }
98
59
  })
99
- }
100
60
 
101
- private updateTypingUsers(roomId: string, user: string, typing: boolean) {
102
- const current = this.state.typingUsers[roomId] || []
103
- let updated: string[]
104
-
105
- if (typing && !current.includes(user)) {
106
- updated = [...current, user]
107
- } else if (!typing) {
108
- updated = current.filter(u => u !== user)
109
- } else {
110
- return // Sem mudança
111
- }
112
-
113
- this.setState({
114
- typingUsers: {
115
- ...this.state.typingUsers,
116
- [roomId]: updated
117
- }
118
- })
61
+ return { success: true, roomId }
119
62
  }
120
63
 
121
- /**
122
- * Sair de uma sala
123
- */
124
64
  async leaveRoom(payload: { roomId: string }) {
125
65
  const { roomId } = payload
126
66
 
127
- // Sair da sala
67
+ // Limpar listeners
68
+ this.roomListeners.get(roomId)?.forEach(fn => fn())
69
+ this.roomListeners.delete(roomId)
128
70
  this.$room(roomId).leave()
129
71
 
130
- // Atualizar lista
72
+ // Atualizar estado
131
73
  const rooms = this.state.rooms.filter(r => r.id !== roomId)
132
- const activeRoom = this.state.activeRoom === roomId
133
- ? (rooms[0]?.id || null)
134
- : this.state.activeRoom
135
-
136
- // Remover mensagens e typing da sala
137
- const { [roomId]: _msgs, ...restMessages } = this.state.messages
138
- const { [roomId]: _typing, ...restTyping } = this.state.typingUsers
74
+ const { [roomId]: _, ...restMessages } = this.state.messages
139
75
 
140
76
  this.setState({
141
77
  rooms,
142
- activeRoom,
143
- messages: restMessages,
144
- typingUsers: restTyping
78
+ activeRoom: this.state.activeRoom === roomId ? (rooms[0]?.id || null) : this.state.activeRoom,
79
+ messages: restMessages
145
80
  })
146
81
 
147
- return {
148
- success: true,
149
- rooms: this.$rooms
150
- }
82
+ return { success: true }
151
83
  }
152
84
 
153
- /**
154
- * Trocar sala ativa (para UI)
155
- */
156
85
  async switchRoom(payload: { roomId: string }) {
157
- if (!this.$rooms.includes(payload.roomId)) {
158
- throw new Error('Not in this room')
159
- }
160
-
161
- this.setState({ activeRoom: payload.roomId })
86
+ if (!this.$rooms.includes(payload.roomId)) throw new Error('Not in this room')
87
+ this.state.activeRoom = payload.roomId
162
88
  return { success: true }
163
89
  }
164
90
 
165
- // ===== Mensagens =====
166
-
167
- /**
168
- * Enviar mensagem para sala ativa
169
- */
170
- async sendMessage(payload: { text: string; roomId?: string }) {
171
- const roomId = payload.roomId || this.state.activeRoom
91
+ async sendMessage(payload: { text: string }) {
92
+ const roomId = this.state.activeRoom
172
93
  if (!roomId) throw new Error('No active room')
173
94
 
174
95
  const text = payload.text?.trim()
175
96
  if (!text) throw new Error('Message cannot be empty')
176
- if (text.length > 1000) throw new Error('Message too long')
177
97
 
178
98
  const message: ChatMessage = {
179
99
  id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
@@ -182,104 +102,26 @@ export class LiveRoomChat extends LiveComponent<typeof LiveRoomChat.defaultState
182
102
  timestamp: Date.now()
183
103
  }
184
104
 
185
- // Adicionar ao meu estado local
186
- this.addMessageToState(roomId, message)
187
-
188
- // Emitir para outros na sala (eles vão receber via $room.on)
105
+ // Adicionar localmente e emitir para outros
106
+ const msgs = this.state.messages[roomId] || []
107
+ this.setState({
108
+ messages: { ...this.state.messages, [roomId]: [...msgs, message].slice(-100) }
109
+ })
189
110
  this.$room(roomId).emit('message:new', message)
190
111
 
191
- // Parar de digitar
192
- this.stopTyping({ roomId })
193
-
194
112
  return { success: true, message }
195
113
  }
196
114
 
197
- /**
198
- * Indicar que está digitando
199
- */
200
- async startTyping(payload: { roomId?: string }) {
201
- const roomId = payload.roomId || this.state.activeRoom
202
- if (!roomId) return { success: false }
203
-
204
- // Emitir evento
205
- this.$room(roomId).emit('user:typing', {
206
- user: this.state.username,
207
- typing: true
208
- })
209
-
210
- // Limpar timer anterior
211
- const existingTimer = this.typingTimers.get(roomId)
212
- if (existingTimer) clearTimeout(existingTimer)
213
-
214
- // Auto-parar após 3 segundos
215
- const timer = setTimeout(() => {
216
- this.stopTyping({ roomId })
217
- }, 3000)
218
- this.typingTimers.set(roomId, timer)
219
-
220
- return { success: true }
221
- }
222
-
223
- /**
224
- * Parar de digitar
225
- */
226
- async stopTyping(payload: { roomId?: string }) {
227
- const roomId = payload.roomId || this.state.activeRoom
228
- if (!roomId) return { success: false }
229
-
230
- // Limpar timer
231
- const timer = this.typingTimers.get(roomId)
232
- if (timer) {
233
- clearTimeout(timer)
234
- this.typingTimers.delete(roomId)
235
- }
236
-
237
- // Emitir evento
238
- this.$room(roomId).emit('user:typing', {
239
- user: this.state.username,
240
- typing: false
241
- })
242
-
243
- return { success: true }
244
- }
245
-
246
- // ===== Configuração =====
247
-
248
- /**
249
- * Definir nome do usuário
250
- */
251
115
  async setUsername(payload: { username: string }) {
252
116
  const username = payload.username?.trim()
253
- if (!username) throw new Error('Username required')
254
- if (username.length > 30) throw new Error('Username too long')
255
-
256
- this.setState({ username })
257
- return { success: true, username }
258
- }
259
-
260
- /**
261
- * Obter estado de uma sala
262
- */
263
- async getRoomState(payload: { roomId: string }) {
264
- const state = this.$room(payload.roomId).state
265
- return {
266
- success: true,
267
- state: {
268
- messages: state.messages || [],
269
- typingUsers: state.typingUsers || []
270
- }
271
- }
117
+ if (!username || username.length > 30) throw new Error('Invalid username')
118
+ this.state.username = username
119
+ return { success: true }
272
120
  }
273
121
 
274
- // ===== Cleanup =====
275
-
276
- public destroy() {
277
- // Limpar timers
278
- for (const timer of this.typingTimers.values()) {
279
- clearTimeout(timer)
280
- }
281
- this.typingTimers.clear()
282
-
122
+ destroy() {
123
+ for (const fns of this.roomListeners.values()) fns.forEach(fn => fn())
124
+ this.roomListeners.clear()
283
125
  super.destroy()
284
126
  }
285
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fluxstack",
3
- "version": "1.12.0",
3
+ "version": "1.12.1",
4
4
  "description": "⚡ Revolutionary full-stack TypeScript framework with Declarative Config System, Elysia + React + Bun",
5
5
  "keywords": [
6
6
  "framework",