You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
125 lines
5.2 KiB
125 lines
5.2 KiB
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
<title>SA-MP Chat</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { background: #0a0e1a; color: #e0e0e0; font-family: 'Segoe UI', system-ui, sans-serif; height: 100vh; height: 100dvh; display: flex; flex-direction: column; overflow: hidden; }
|
|
.header { background: #111827; padding: 12px 16px; display: flex; align-items: center; gap: 12px; border-bottom: 1px solid #1f2937; flex-shrink: 0; }
|
|
.header a { color: #6b7280; text-decoration: none; font-size: 20px; }
|
|
.header h1 { font-size: 16px; font-weight: 600; color: #f3f4f6; }
|
|
.status { margin-left: auto; width: 8px; height: 8px; border-radius: 50%; background: #ef4444; transition: background 0.3s; }
|
|
.status.on { background: #22c55e; }
|
|
.messages { flex: 1; overflow-y: auto; padding: 8px 12px; display: flex; flex-direction: column; gap: 2px; }
|
|
.msg { padding: 4px 8px; border-radius: 4px; font-family: 'Consolas', 'SF Mono', monospace; font-size: 13px; line-height: 1.5; word-wrap: break-word; animation: fi 0.15s ease; }
|
|
.msg:hover { background: rgba(255,255,255,0.03); }
|
|
.msg .t { color: #4b5563; font-size: 11px; margin-right: 6px; user-select: none; }
|
|
.msg.me { border-left: 2px solid #3b82f6; background: rgba(59,130,246,0.06); }
|
|
@keyframes fi { from { opacity:0; transform:translateY(4px); } to { opacity:1; transform:translateY(0); } }
|
|
.input-area { background: #111827; padding: 10px 12px; border-top: 1px solid #1f2937; display: flex; gap: 8px; flex-shrink: 0; }
|
|
.input-area input { flex: 1; background: #1f2937; border: 1px solid #374151; border-radius: 8px; padding: 10px 14px; color: #f3f4f6; font-size: 14px; outline: none; }
|
|
.input-area input:focus { border-color: #3b82f6; }
|
|
.input-area input::placeholder { color: #6b7280; }
|
|
.input-area button { background: #3b82f6; color: white; border: none; border-radius: 8px; padding: 10px 20px; font-size: 14px; font-weight: 600; cursor: pointer; }
|
|
.input-area button:hover { background: #2563eb; }
|
|
.input-area button:active { background: #1d4ed8; }
|
|
.messages::-webkit-scrollbar { width: 6px; }
|
|
.messages::-webkit-scrollbar-track { background: transparent; }
|
|
.messages::-webkit-scrollbar-thumb { background: #374151; border-radius: 3px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<a href="/">←</a>
|
|
<h1>SA-MP Chat</h1>
|
|
<div class="status" id="st"></div>
|
|
</div>
|
|
<div class="messages" id="msgs"></div>
|
|
<div class="input-area">
|
|
<input type="text" id="inp" placeholder="Type a message..." maxlength="144" autocomplete="off">
|
|
<button onclick="send()">Send</button>
|
|
</div>
|
|
<script>
|
|
const msgs = document.getElementById('msgs');
|
|
const inp = document.getElementById('inp');
|
|
const st = document.getElementById('st');
|
|
let ws, atBottom = true;
|
|
|
|
msgs.addEventListener('scroll', () => {
|
|
atBottom = msgs.scrollHeight - msgs.scrollTop - msgs.clientHeight < 40;
|
|
});
|
|
|
|
function scroll() { if (atBottom) msgs.scrollTop = msgs.scrollHeight; }
|
|
|
|
function colorToCSS(c) {
|
|
let u = c >>> 0, r = (u>>24)&0xFF, g = (u>>16)&0xFF, b = (u>>8)&0xFF, a = u&0xFF;
|
|
if (!a) a = 255;
|
|
return `rgba(${r},${g},${b},${(a/255).toFixed(2)})`;
|
|
}
|
|
|
|
function parseColors(text) {
|
|
text = text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
let r = '', parts = text.split(/(\{[0-9A-Fa-f]{6}\})/), inSpan = false;
|
|
for (let p of parts) {
|
|
let m = p.match(/^\{([0-9A-Fa-f]{6})\}$/);
|
|
if (m) { if (inSpan) r += '</span>'; r += `<span style="color:#${m[1]}">`; inSpan = true; }
|
|
else r += p;
|
|
}
|
|
if (inSpan) r += '</span>';
|
|
return r;
|
|
}
|
|
|
|
function addMsg(ev, args) {
|
|
if (ev !== 'onServerMessage') return;
|
|
const [color, text] = args;
|
|
const d = document.createElement('div');
|
|
d.className = 'msg';
|
|
const now = new Date().toTimeString().slice(0,8);
|
|
const css = color ? ` style="color:${colorToCSS(color)}"` : '';
|
|
d.innerHTML = `<span class="t">${now}</span><span${css}>${parseColors(text)}</span>`;
|
|
msgs.appendChild(d);
|
|
while (msgs.children.length > 300) msgs.removeChild(msgs.firstChild);
|
|
scroll();
|
|
}
|
|
|
|
function addSent(text) {
|
|
const d = document.createElement('div');
|
|
d.className = 'msg me';
|
|
const now = new Date().toTimeString().slice(0,8);
|
|
d.innerHTML = `<span class="t">${now}</span><span style="color:#93c5fd">${text.replace(/</g,'<')}</span>`;
|
|
msgs.appendChild(d);
|
|
scroll();
|
|
}
|
|
|
|
function send() {
|
|
const text = inp.value.trim();
|
|
if (!text) return;
|
|
fetch('/api/chat/send', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({text}) });
|
|
addSent(text);
|
|
inp.value = '';
|
|
inp.focus();
|
|
}
|
|
|
|
inp.addEventListener('keydown', e => { if (e.key === 'Enter') send(); });
|
|
|
|
function connect() {
|
|
ws = new WebSocket('ws://'+location.host+'/ws');
|
|
ws.onopen = () => st.className = 'status on';
|
|
ws.onclose = () => { st.className = 'status'; setTimeout(connect, 2000); };
|
|
ws.onerror = () => ws.close();
|
|
ws.onmessage = e => {
|
|
try {
|
|
const d = JSON.parse(e.data);
|
|
if (d.type === 'event') addMsg(d.event, d.args);
|
|
} catch(err) {}
|
|
};
|
|
}
|
|
|
|
connect();
|
|
inp.focus();
|
|
</script>
|
|
</body>
|
|
</html>
|