mirror of
https://github.com/fishtank-dashboard/fishtank-dashboard.git
synced 2026-04-30 09:12:04 -04:00
Delete chat-popout.html
This commit is contained in:
committed by
GitHub
parent
4f3624dd33
commit
55882b2ed4
383
chat-popout.html
383
chat-popout.html
@@ -1,383 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>FISHTANK // CHAT</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Bebas+Neue&family=DM+Sans:wght@300;400;500&display=swap');
|
||||
|
||||
:root {
|
||||
--bg: #080b0f;
|
||||
--panel: #0d1117;
|
||||
--border: #1a2332;
|
||||
--accent: #00e5ff;
|
||||
--accent2: #ff3d71;
|
||||
--accent3: #ffe600;
|
||||
--text: #c9d4e0;
|
||||
--muted: #4a5568;
|
||||
--green: #00ff88;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--panel);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: 'Bebas Neue', sans-serif;
|
||||
font-size: 18px;
|
||||
letter-spacing: 3px;
|
||||
color: var(--accent);
|
||||
}
|
||||
.logo span { color: var(--accent2); }
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 7px; height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--muted);
|
||||
}
|
||||
.dot.live {
|
||||
background: var(--green);
|
||||
box-shadow: 0 0 8px var(--green);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
.dot.error { background: var(--accent2); }
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.msg-count {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
#chatFeed {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#chatFeed::-webkit-scrollbar { width: 4px; }
|
||||
#chatFeed::-webkit-scrollbar-track { background: transparent; }
|
||||
#chatFeed::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||
|
||||
.msg {
|
||||
padding: 7px 14px;
|
||||
border-bottom: 1px solid rgba(26,35,50,0.5);
|
||||
animation: slideIn 0.2s ease;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.msg:hover { background: rgba(255,255,255,0.02); }
|
||||
|
||||
@keyframes slideIn {
|
||||
from { opacity: 0; transform: translateY(-4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.msg-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 7px;
|
||||
margin-bottom: 2px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.msg-user {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.msg-clan {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 9px;
|
||||
color: var(--muted);
|
||||
background: rgba(255,255,255,0.05);
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.msg-medals {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.msg-endorsement {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 9px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.msg-time {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 9px;
|
||||
color: var(--muted);
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.msg-text {
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.msg-text .mention {
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.msg.fish { border-left: 2px solid var(--accent3); background: rgba(255,230,0,0.04); }
|
||||
.msg.admin { border-left: 2px solid var(--accent2); background: rgba(255,61,113,0.06); }
|
||||
.msg.mod { border-left: 2px solid var(--green); background: rgba(0,255,136,0.05); }
|
||||
|
||||
.badge {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 8px;
|
||||
padding: 1px 5px;
|
||||
border-radius: 2px;
|
||||
letter-spacing: 1px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.badge.admin { background: rgba(255,61,113,0.2); color: var(--accent2); border: 1px solid rgba(255,61,113,0.4); }
|
||||
.badge.mod { background: rgba(0,255,136,0.1); color: var(--green); border: 1px solid rgba(0,255,136,0.3); }
|
||||
.badge.fish { background: rgba(255,230,0,0.1); color: var(--accent3); border: 1px solid rgba(255,230,0,0.3); }
|
||||
.badge.gm { background: rgba(255,61,113,0.15); color: var(--accent2); border: 1px solid rgba(255,61,113,0.3); }
|
||||
|
||||
.empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--muted);
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
.empty-icon { font-size: 32px; opacity: 0.3; }
|
||||
|
||||
.scroll-btn {
|
||||
position: fixed;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 10px;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
letter-spacing: 1px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s;
|
||||
z-index: 10;
|
||||
}
|
||||
.scroll-btn.visible { opacity: 1; pointer-events: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<div class="logo">FISH<span>TANK</span> // CHAT</div>
|
||||
<div class="status">
|
||||
<div class="dot" id="wsDot"></div>
|
||||
<span id="wsStatus">CONNECTING</span>
|
||||
</div>
|
||||
<div class="msg-count" id="msgCount">0 messages</div>
|
||||
</header>
|
||||
|
||||
<div id="chatFeed">
|
||||
<div class="empty">
|
||||
<div class="empty-icon">💬</div>
|
||||
<span>Waiting for messages...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="scroll-btn" id="scrollBtn" onclick="scrollToBottom()">▼ NEW MESSAGES</button>
|
||||
|
||||
<script>
|
||||
const feed = document.getElementById('chatFeed');
|
||||
const scrollBtn = document.getElementById('scrollBtn');
|
||||
let msgCount = 0;
|
||||
let autoScroll = true;
|
||||
let ws = null;
|
||||
let reconnectTimer = null;
|
||||
|
||||
feed.addEventListener('scroll', () => {
|
||||
const atBottom = feed.scrollHeight - feed.scrollTop - feed.clientHeight < 60;
|
||||
autoScroll = atBottom;
|
||||
scrollBtn.classList.toggle('visible', !atBottom);
|
||||
});
|
||||
|
||||
function scrollToBottom() {
|
||||
feed.scrollTop = feed.scrollHeight;
|
||||
autoScroll = true;
|
||||
scrollBtn.classList.remove('visible');
|
||||
}
|
||||
|
||||
function setStatus(state) {
|
||||
const dot = document.getElementById('wsDot');
|
||||
const label = document.getElementById('wsStatus');
|
||||
const map = {
|
||||
connected: { cls: 'live', text: 'LIVE' },
|
||||
disconnected: { cls: 'error', text: 'OFFLINE' },
|
||||
connecting: { cls: '', text: 'CONNECTING' },
|
||||
};
|
||||
const s = map[state] || map.connecting;
|
||||
dot.className = 'dot ' + s.cls;
|
||||
label.textContent = s.text;
|
||||
label.style.color = s.cls === 'live' ? 'var(--green)' : s.cls === 'error' ? 'var(--accent2)' : 'var(--muted)';
|
||||
}
|
||||
|
||||
function formatTime(ts) {
|
||||
return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
}
|
||||
|
||||
function isRealChatMessage(msg) {
|
||||
// Filter out TTS messages (user.id === 'tts')
|
||||
if (msg.user && msg.user.id === 'tts') return false;
|
||||
// Filter out item use messages (metadata.type === 'item')
|
||||
if (msg.metadata && msg.metadata.type === 'item') return false;
|
||||
// Must have a real message string
|
||||
if (!msg.message || typeof msg.message !== 'string') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function addMessage(msg) {
|
||||
const empty = feed.querySelector('.empty');
|
||||
if (empty) empty.remove();
|
||||
|
||||
const user = msg.user || {};
|
||||
const meta = msg.metadata || {};
|
||||
const isFish = meta.isFish || false;
|
||||
const isGM = meta.isGrandMarshall || false;
|
||||
const isEpic = meta.isEpic || false;
|
||||
const isAdmin = meta.isAdmin || false;
|
||||
const isMod = meta.isMod || false;
|
||||
|
||||
const cls = ['msg',
|
||||
isAdmin ? 'admin' : '',
|
||||
isMod ? 'mod' : '',
|
||||
isFish ? 'fish' : '',
|
||||
isGM ? 'grand-marshall' : '',
|
||||
isEpic ? 'epic' : '',
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
// Username color: custom > endorsement color > default
|
||||
let nameStyle = '';
|
||||
if (user.customUsernameColor) {
|
||||
nameStyle = `style="color:${user.customUsernameColor}"`;
|
||||
}
|
||||
|
||||
// Endorsement tag (replaces medals)
|
||||
const endorsement = user.endorsement
|
||||
? `<span class="msg-endorsement" style="color:${user.endorsementColor || '#888'}">${user.endorsement}</span>`
|
||||
: '';
|
||||
|
||||
// Role badges for special users only
|
||||
const badges = [
|
||||
isAdmin ? '<span class="badge admin">ADMIN</span>' : '',
|
||||
isMod ? '<span class="badge mod">MOD</span>' : '',
|
||||
isFish ? '<span class="badge fish">FISH</span>' : '',
|
||||
isGM ? '<span class="badge gm">GM</span>' : '',
|
||||
].filter(Boolean).join('');
|
||||
|
||||
const clan = user.clan ? `<span class="msg-clan">[${user.clan}]</span>` : '';
|
||||
const ts = msg.timestamp ? formatTime(msg.timestamp) : '';
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = cls;
|
||||
div.innerHTML = `
|
||||
<div class="msg-header">
|
||||
<span class="msg-user" ${nameStyle}>${user.displayName || 'unknown'}</span>
|
||||
${clan}
|
||||
${endorsement}
|
||||
${badges}
|
||||
${ts ? `<span class="msg-time">${ts}</span>` : ''}
|
||||
</div>
|
||||
<div class="msg-text">${escapeHtml(String(msg.message))}</div>`;
|
||||
|
||||
feed.appendChild(div);
|
||||
msgCount++;
|
||||
document.getElementById('msgCount').textContent = msgCount.toLocaleString() + ' messages';
|
||||
if (autoScroll) feed.scrollTop = feed.scrollHeight;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
function connect() {
|
||||
if (ws) { ws.close(); ws = null; }
|
||||
setStatus('connecting');
|
||||
ws = new WebSocket('ws://localhost:3000');
|
||||
|
||||
ws.addEventListener('open', () => setStatus('connected'));
|
||||
|
||||
ws.addEventListener('message', e => {
|
||||
let msg;
|
||||
try { msg = JSON.parse(e.data); } catch { return; }
|
||||
if (msg._ft === 'ws_status') {
|
||||
setStatus(msg.status === 'connected' ? 'connected' : 'disconnected');
|
||||
return;
|
||||
}
|
||||
if (msg._ft === 'event' && msg.event === 'chat:message') {
|
||||
const data = msg.data;
|
||||
const messages = Array.isArray(data) ? data : [data];
|
||||
messages.forEach(m => { if (isRealChatMessage(m)) addMessage(m); });
|
||||
}
|
||||
});
|
||||
|
||||
ws.addEventListener('close', () => {
|
||||
setStatus('disconnected');
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||
reconnectTimer = setTimeout(connect, 3000);
|
||||
});
|
||||
|
||||
ws.addEventListener('error', () => setStatus('disconnected'));
|
||||
}
|
||||
|
||||
connect();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user