@fuzionx/framework 0.1.45 → 0.1.47
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/README.md +29 -2
- package/cli/index.js +37 -8
- package/cli/templates/make/app-spa/controllers/AuthController.js +114 -0
- package/cli/templates/make/app-spa/controllers/HomeController.js +66 -0
- package/cli/templates/make/app-spa/controllers/PostController.js +191 -0
- package/cli/templates/make/app-spa/controllers/UserController.js +43 -0
- package/cli/templates/make/app-spa/public/css/style.css +1011 -0
- package/cli/templates/make/app-spa/routes/api.js +31 -0
- package/cli/templates/make/app-spa/routes/web.js +19 -0
- package/cli/templates/make/app-spa/services/AuthService.js +48 -0
- package/cli/templates/make/app-spa/services/PostService.js +372 -0
- package/cli/templates/make/app-spa/services/UserService.js +48 -0
- package/cli/templates/make/app-spa/views/default/errors/404.html +11 -0
- package/cli/templates/make/app-spa/views/default/errors/500.html +11 -0
- package/cli/templates/make/app-spa/views/default/layouts/main.html +34 -0
- package/cli/templates/make/app-spa/views/default/pages/home.html +22 -0
- package/cli/templates/make/app-spa/views/default/spa/index.html +13 -0
- package/cli/templates/make/app-spa/views/default/spa/package.json +20 -0
- package/cli/templates/make/app-spa/views/default/spa/src/App.vue +41 -0
- package/cli/templates/make/app-spa/views/default/spa/src/assets/landing.css +220 -0
- package/cli/templates/make/app-spa/views/default/spa/src/assets/style.css +1156 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/AlertDialog.vue +179 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/CodeBlock.vue +33 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/EditorToolbar.vue +54 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/FileUpload.vue +161 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/FlashMessage.vue +39 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/LanguageSwitcher.vue +108 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/Lightbox.vue +62 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/Navbar.vue +68 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/Pagination.vue +166 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/ToastContainer.vue +135 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useApi.js +129 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useClipboard.js +44 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useDate.js +73 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useDebounce.js +59 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useFlash.js +46 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useHeartbeat.js +45 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useLocalStorage.js +43 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useLocale.js +79 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useWebSocket.js +93 -0
- package/cli/templates/make/app-spa/views/default/spa/src/main.js +106 -0
- package/cli/templates/make/app-spa/views/default/spa/src/plugins/alert.js +96 -0
- package/cli/templates/make/app-spa/views/default/spa/src/plugins/toast.js +79 -0
- package/cli/templates/make/app-spa/views/default/spa/src/router/index.js +29 -0
- package/cli/templates/make/app-spa/views/default/spa/src/stores/auth.js +58 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/BoardDetail.vue +169 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/BoardForm.vue +192 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/BoardList.vue +129 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/ChatView.vue +317 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/FeaturesView.vue +242 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/HomeView.vue +215 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/Login.vue +82 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/Profile.vue +85 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/Register.vue +84 -0
- package/cli/templates/make/app-spa/views/default/spa/vite.config.js +28 -0
- package/cli/templates/make/app-spa/views/default/spa/yarn.lock +633 -0
- package/cli/templates/make/app-spa/ws/ChatHandler.js +138 -0
- package/cli/templates/make/app-ssr/controllers/AuthController.js +119 -0
- package/cli/templates/make/app-ssr/controllers/ChatController.js +15 -0
- package/cli/templates/make/app-ssr/controllers/FeaturesController.js +15 -0
- package/cli/templates/make/app-ssr/controllers/HomeController.js +21 -0
- package/cli/templates/make/app-ssr/controllers/PostController.js +214 -0
- package/cli/templates/make/app-ssr/controllers/UserController.js +48 -0
- package/cli/templates/make/app-ssr/public/css/fx-ui.css +43 -0
- package/cli/templates/make/app-ssr/public/css/landing.css +220 -0
- package/cli/templates/make/app-ssr/public/css/style.css +1011 -0
- package/cli/templates/make/app-ssr/public/js/fx-client.js +107 -0
- package/cli/templates/make/app-ssr/public/js/fx-ui.js +124 -0
- package/cli/templates/make/app-ssr/routes/web.js +46 -0
- package/cli/templates/make/app-ssr/services/AuthService.js +48 -0
- package/cli/templates/make/app-ssr/services/PostService.js +372 -0
- package/cli/templates/make/app-ssr/services/UserService.js +48 -0
- package/cli/templates/make/app-ssr/views/default/errors/404.html +11 -0
- package/cli/templates/make/app-ssr/views/default/errors/500.html +48 -0
- package/cli/templates/make/app-ssr/views/default/layouts/main.html +96 -0
- package/cli/templates/make/app-ssr/views/default/pages/board/form.html +240 -0
- package/cli/templates/make/app-ssr/views/default/pages/board/index.html +73 -0
- package/cli/templates/make/app-ssr/views/default/pages/board/show.html +148 -0
- package/cli/templates/make/app-ssr/views/default/pages/chat.html +288 -0
- package/cli/templates/make/app-ssr/views/default/pages/features.html +373 -0
- package/cli/templates/make/app-ssr/views/default/pages/home.html +258 -0
- package/cli/templates/make/app-ssr/views/default/pages/login.html +27 -0
- package/cli/templates/make/app-ssr/views/default/pages/profile.html +36 -0
- package/cli/templates/make/app-ssr/views/default/pages/register.html +35 -0
- package/cli/templates/make/app-ssr/views/default/partials/pagination.html +75 -0
- package/cli/templates/make/app-ssr/ws/ChatHandler.js +138 -0
- package/lib/core/Application.js +425 -138
- package/lib/core/Context.js +540 -236
- package/lib/middleware/auth.js +1 -1
- package/lib/middleware/csrf.js +1 -1
- package/lib/middleware/session.js +5 -4
- package/package.json +2 -2
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
{% extends "layouts/main.html" %}
|
|
2
|
+
{% block title %}{{ t(key="chat.page_title", default="Chat Demo") }} — FuzionX WebSocket{% endblock %}
|
|
3
|
+
{% block description %}{{ t(key="chat.meta_desc", default="Live WebSocket chat demo powered by FuzionX WsHandler. Try the real-time messaging with user presence.") }}{% endblock %}
|
|
4
|
+
{% block content %}
|
|
5
|
+
|
|
6
|
+
<div class="container">
|
|
7
|
+
<!-- ── Join Screen ── -->
|
|
8
|
+
<div id="chat-join" class="chat-join-screen">
|
|
9
|
+
<div class="glass-card chat-join-card">
|
|
10
|
+
<div class="chat-join-icon">💬</div>
|
|
11
|
+
<h1 class="chat-join-title">WebSocket <span class="gradient-text">{{ t(key="chat.demo_title", default="Chat Demo") }}</span></h1>
|
|
12
|
+
<p class="chat-join-sub">{{ t(key="chat.demo_sub", default="Experience FuzionX real-time WebSocket in action.") }}<br>{{ t(key="chat.demo_tech", default="Built with") }} <code>WsHandler</code> + <code>FuzionXSocket</code> WASM client.</p>
|
|
13
|
+
<div class="form-group" style="margin-top:2rem;">
|
|
14
|
+
<input type="text" id="nickname-input" class="chat-nickname-input" placeholder="{{ t(key="chat.nickname_placeholder", default="Enter your nickname...") }}" maxlength="20" autocomplete="off">
|
|
15
|
+
</div>
|
|
16
|
+
<button id="join-btn" class="btn btn-hero-primary btn-full" onclick="joinChat()">
|
|
17
|
+
<span>{{ t(key="chat.btn_join", default="Join Chat") }}</span>
|
|
18
|
+
</button>
|
|
19
|
+
<div class="chat-join-tech">
|
|
20
|
+
<span class="feature-tag tag-ws">FuzionXSocket</span>
|
|
21
|
+
<span class="feature-tag tag-rust">WASM</span>
|
|
22
|
+
<span class="feature-tag tag-node">Auto ASP</span>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<!-- ── Chat Room ── -->
|
|
28
|
+
<div id="chat-room" class="chat-room" style="display:none;">
|
|
29
|
+
<div class="chat-layout">
|
|
30
|
+
<!-- Sidebar: User List -->
|
|
31
|
+
<aside class="chat-sidebar glass-card">
|
|
32
|
+
<div class="chat-sidebar-header">
|
|
33
|
+
<h3>{{ t(key="chat.online", default="Online") }} <span id="user-count" class="chat-user-count">0</span></h3>
|
|
34
|
+
</div>
|
|
35
|
+
<ul id="user-list" class="chat-user-list">
|
|
36
|
+
</ul>
|
|
37
|
+
<div class="chat-sidebar-footer">
|
|
38
|
+
<button class="btn btn-outline btn-full" onclick="leaveChat()" style="font-size:0.85rem;">{{ t(key="chat.btn_leave", default="Leave Chat") }}</button>
|
|
39
|
+
</div>
|
|
40
|
+
</aside>
|
|
41
|
+
|
|
42
|
+
<!-- Main: Messages -->
|
|
43
|
+
<div class="chat-main glass-card">
|
|
44
|
+
<div class="chat-messages-header">
|
|
45
|
+
<h3>💬 {{ t(key="chat.room_title", default="Chat Room") }}</h3>
|
|
46
|
+
<span id="connection-status" class="chat-status chat-status-connecting">{{ t(key="chat.connecting", default="connecting...") }}</span>
|
|
47
|
+
</div>
|
|
48
|
+
<div id="chat-messages" class="chat-messages">
|
|
49
|
+
</div>
|
|
50
|
+
<div id="typing-indicator" class="chat-typing" style="display:none;"></div>
|
|
51
|
+
<form id="chat-form" class="chat-input-form" onsubmit="sendMessage(event)">
|
|
52
|
+
<input type="text" id="message-input" class="chat-message-input" placeholder="{{ t(key="chat.msg_placeholder", default="Type a message...") }}" autocomplete="off" maxlength="500">
|
|
53
|
+
<button type="submit" class="btn btn-primary chat-send-btn">{{ t(key="chat.btn_send", default="Send") }}</button>
|
|
54
|
+
</form>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<footer class="site-footer">
|
|
61
|
+
<div class="container">
|
|
62
|
+
<div class="footer-content">
|
|
63
|
+
<span class="footer-brand">FuzionX</span>
|
|
64
|
+
<span class="footer-text">{{ t(key="chat.footer", default="Powered by WsHandler") }} — <a href="/features#websocket" style="color:var(--accent-solid);">{{ t(key="chat.footer_link", default="WebSocket Features") }}</a></span>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</footer>
|
|
68
|
+
|
|
69
|
+
{% endblock %}
|
|
70
|
+
|
|
71
|
+
{% block scripts %}
|
|
72
|
+
<script>
|
|
73
|
+
let ws = null;
|
|
74
|
+
let mySessionId = null;
|
|
75
|
+
let myNickname = null;
|
|
76
|
+
let typingTimeout = null;
|
|
77
|
+
const JOIN_BTN_TEXT = '{{ t(key="chat.btn_join", default="Join Chat") }}';
|
|
78
|
+
const ESC_MAP = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
79
|
+
|
|
80
|
+
function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ESC_MAP[c]); }
|
|
81
|
+
|
|
82
|
+
async function joinChat() {
|
|
83
|
+
const input = document.getElementById('nickname-input');
|
|
84
|
+
const name = (input.value || '').trim();
|
|
85
|
+
if (!name) { input.focus(); input.style.borderColor = '#e74c3c'; return; }
|
|
86
|
+
myNickname = name;
|
|
87
|
+
const btn = document.getElementById('join-btn');
|
|
88
|
+
btn.disabled = true;
|
|
89
|
+
btn.querySelector('span').textContent = '{{ t(key="chat.connecting", default="connecting...") }}';
|
|
90
|
+
await connectWS();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 로그인 사용자 닉네임 자동 입력 (자동 참여 안 함)
|
|
94
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
95
|
+
{% if auth.user %}
|
|
96
|
+
const userName = '{{ auth.user.name | default(value="") }}';
|
|
97
|
+
if (userName) {
|
|
98
|
+
document.getElementById('nickname-input').value = userName;
|
|
99
|
+
}
|
|
100
|
+
{% endif %}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
async function connectWS() {
|
|
104
|
+
const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
105
|
+
const url = `${wsProto}//${location.host}/ws/chat`;
|
|
106
|
+
|
|
107
|
+
// WASM 로드 대기
|
|
108
|
+
await window._aspReady;
|
|
109
|
+
|
|
110
|
+
// FuzionXSocket이 아직 없으면 직접 WASM 로드
|
|
111
|
+
if (!window.FuzionXSocket) {
|
|
112
|
+
try {
|
|
113
|
+
const _v = Date.now();
|
|
114
|
+
const mod = await import(`/wasm/fuzionx_client_wasm.js?v=${_v}`);
|
|
115
|
+
await mod.default({ module_or_path: `/wasm/fuzionx_client_wasm_bg.wasm?v=${_v}` });
|
|
116
|
+
window.FuzionXSocket = mod.FuzionXSocket;
|
|
117
|
+
} catch (e) {
|
|
118
|
+
console.error('[Chat] WASM load failed:', e);
|
|
119
|
+
resetJoinBtn();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
ws = new window.FuzionXSocket(url, '{{ config.bridge.asp.master_secret | default(value="") }}', {{ config.bridge.asp.enabled | default(value="false") }} || {{ config.app.asp.enabled | default(value="false") }});
|
|
125
|
+
|
|
126
|
+
ws.on('connect', () => {
|
|
127
|
+
setStatus('connected', window.aspEnabled ? 'connected (ASP 🔐)' : 'connected');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
ws.on('message', (raw) => {
|
|
131
|
+
console.log('[Chat] raw:', JSON.stringify(raw));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
ws.on('chat_ready', (data) => {
|
|
135
|
+
if (!data || !data.sessionId) return;
|
|
136
|
+
mySessionId = data.sessionId;
|
|
137
|
+
|
|
138
|
+
// 화면 전환
|
|
139
|
+
document.getElementById('chat-join').style.display = 'none';
|
|
140
|
+
document.getElementById('chat-room').style.display = 'block';
|
|
141
|
+
document.getElementById('message-input').focus();
|
|
142
|
+
appendSystemMessage(`${myNickname} 접속`);
|
|
143
|
+
|
|
144
|
+
// 서버에 사용자 정보 전송 — 응답으로 userlist가 포함됨
|
|
145
|
+
ws.send('setUser', {
|
|
146
|
+
name: myNickname,
|
|
147
|
+
{% if auth.user %}
|
|
148
|
+
email: '{{ auth.user.email | default(value="") }}',
|
|
149
|
+
id: {{ auth.user.id | default(value="0") }},
|
|
150
|
+
{% endif %}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
ws.on('userlist', (data) => { renderUserList(data); });
|
|
155
|
+
ws.on('user_joined', (data) => {
|
|
156
|
+
addUserToList(data);
|
|
157
|
+
appendSystemMessage(`${data.name || 'unknown'} 입장`);
|
|
158
|
+
});
|
|
159
|
+
ws.on('user_left', (data) => {
|
|
160
|
+
removeUserFromList(data);
|
|
161
|
+
appendSystemMessage(`${data.name || 'unknown'} 퇴장`);
|
|
162
|
+
});
|
|
163
|
+
ws.on('system', (data) => { appendSystemMessage(data.text); });
|
|
164
|
+
ws.on('typing', (data) => { showTyping(data.user); });
|
|
165
|
+
ws.on('broadcast', (data) => { appendSystemMessage(`📢 ${data.user}: ${data.text}`); });
|
|
166
|
+
ws.on('chat_msg', (data) => {
|
|
167
|
+
appendMessage({ sender: data.user, message: data.text, timestamp: data.timestamp }, false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
ws.on('disconnect', () => { setStatus('disconnected', 'disconnected'); });
|
|
171
|
+
ws.on('reconnect', () => { setStatus('connecting', 'reconnecting...'); });
|
|
172
|
+
ws.on('error', () => { setStatus('error', 'connection error'); resetJoinBtn(); });
|
|
173
|
+
|
|
174
|
+
ws.connect();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function renderUserList(data) {
|
|
178
|
+
const list = document.getElementById('user-list');
|
|
179
|
+
const count = document.getElementById('user-count');
|
|
180
|
+
count.textContent = data.count || 0;
|
|
181
|
+
list.innerHTML = '';
|
|
182
|
+
(data.users || []).forEach(user => {
|
|
183
|
+
list.appendChild(_createUserLi(user));
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function addUserToList(user) {
|
|
188
|
+
const list = document.getElementById('user-list');
|
|
189
|
+
const count = document.getElementById('user-count');
|
|
190
|
+
if (list.querySelector(`[data-sid="${user.sid}"]`)) return;
|
|
191
|
+
list.appendChild(_createUserLi(user));
|
|
192
|
+
count.textContent = list.children.length;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function removeUserFromList(data) {
|
|
196
|
+
const list = document.getElementById('user-list');
|
|
197
|
+
const count = document.getElementById('user-count');
|
|
198
|
+
const el = list.querySelector(`[data-sid="${data.sid}"]`);
|
|
199
|
+
if (el) el.remove();
|
|
200
|
+
count.textContent = list.children.length;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function _createUserLi(user) {
|
|
204
|
+
const li = document.createElement('li');
|
|
205
|
+
const isMe = mySessionId && mySessionId === user.sid;
|
|
206
|
+
li.className = 'chat-user-item' + (isMe ? ' is-me' : '');
|
|
207
|
+
li.setAttribute('data-sid', user.sid);
|
|
208
|
+
li.innerHTML = '<span class="chat-user-dot"></span><span class="chat-user-name">' + escapeHtml(user.name) + '</span>' + (isMe ? '<span class="chat-user-me">you</span>' : '');
|
|
209
|
+
return li;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function appendMessage(data, isMe) {
|
|
213
|
+
const container = document.getElementById('chat-messages');
|
|
214
|
+
const wrapper = document.createElement('div');
|
|
215
|
+
wrapper.className = 'chat-msg ' + (isMe ? 'chat-msg-me' : 'chat-msg-other');
|
|
216
|
+
const time = data.timestamp ? new Date(data.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
|
|
217
|
+
wrapper.innerHTML = '<div class="chat-msg-bubble">' +
|
|
218
|
+
(!isMe ? '<span class="chat-msg-sender">' + escapeHtml(data.sender || '') + '</span>' : '') +
|
|
219
|
+
'<span class="chat-msg-text">' + escapeHtml(data.message || '') + '</span>' +
|
|
220
|
+
'<span class="chat-msg-time">' + time + '</span></div>';
|
|
221
|
+
container.appendChild(wrapper);
|
|
222
|
+
container.scrollTop = container.scrollHeight;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function appendSystemMessage(message) {
|
|
226
|
+
const container = document.getElementById('chat-messages');
|
|
227
|
+
const div = document.createElement('div');
|
|
228
|
+
div.className = 'chat-msg-system';
|
|
229
|
+
div.textContent = message;
|
|
230
|
+
container.appendChild(div);
|
|
231
|
+
container.scrollTop = container.scrollHeight;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function showTyping(user) {
|
|
235
|
+
const indicator = document.getElementById('typing-indicator');
|
|
236
|
+
indicator.textContent = user + ' {{ t(key="chat.typing", default="is typing...") }}';
|
|
237
|
+
indicator.style.display = 'block';
|
|
238
|
+
clearTimeout(typingTimeout);
|
|
239
|
+
typingTimeout = setTimeout(() => { indicator.style.display = 'none'; }, 2000);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function sendMessage(e) {
|
|
243
|
+
e.preventDefault();
|
|
244
|
+
const input = document.getElementById('message-input');
|
|
245
|
+
const text = input.value.trim();
|
|
246
|
+
if (!text || !ws || !ws.is_connected()) return;
|
|
247
|
+
ws.send('message', { text });
|
|
248
|
+
appendMessage({ sender: myNickname, message: text, timestamp: Date.now() }, true);
|
|
249
|
+
input.value = '';
|
|
250
|
+
input.focus();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function leaveChat() {
|
|
254
|
+
if (ws) ws.disconnect();
|
|
255
|
+
ws = null; mySessionId = null;
|
|
256
|
+
document.getElementById('chat-room').style.display = 'none';
|
|
257
|
+
document.getElementById('chat-join').style.display = '';
|
|
258
|
+
document.getElementById('chat-messages').innerHTML = '';
|
|
259
|
+
resetJoinBtn();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function resetJoinBtn() {
|
|
263
|
+
const btn = document.getElementById('join-btn');
|
|
264
|
+
btn.disabled = false;
|
|
265
|
+
btn.querySelector('span').textContent = JOIN_BTN_TEXT;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function setStatus(cls, text) {
|
|
269
|
+
const el = document.getElementById('connection-status');
|
|
270
|
+
if (!el) return;
|
|
271
|
+
el.className = 'chat-status chat-status-' + cls;
|
|
272
|
+
el.textContent = text;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Enter key + typing indicator
|
|
276
|
+
document.getElementById('nickname-input').addEventListener('keydown', e => { if (e.key === 'Enter') joinChat(); });
|
|
277
|
+
document.getElementById('message-input').addEventListener('input', function() {
|
|
278
|
+
if (!ws || !ws.is_connected()) return;
|
|
279
|
+
const now = Date.now();
|
|
280
|
+
if (!this._lastTyping || now - this._lastTyping > 1500) {
|
|
281
|
+
ws.send('typing', {});
|
|
282
|
+
this._lastTyping = now;
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
</script>
|
|
286
|
+
{% endblock %}
|
|
287
|
+
|
|
288
|
+
|