commits
tags
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<title>Kiosk Livechat</title>
<script src="/static/socket.io.min.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
width: 100%; height: 100%;
background: #0f0f0f;
color: #e8e8e8;
font-family: 'Segoe UI', system-ui, sans-serif;
overflow: hidden;
}
/* Layout — chat left, QR panel right */
#root {
display: flex;
height: 100vh;
gap: 0;
}
/* === Chat column === */
#chat-col {
flex: 1;
display: flex;
flex-direction: column;
padding: 1.2rem 1rem 1rem 1.4rem;
overflow: hidden;
}
#chat-header {
display: flex;
align-items: center;
gap: .75rem;
margin-bottom: .9rem;
flex-shrink: 0;
}
#chat-header h1 {
font-size: 1.35rem;
font-weight: 700;
color: #fff;
letter-spacing: .02em;
}
#viewer-count {
font-size: .85rem;
color: #888;
background: #1a1a1a;
padding: .2rem .6rem;
border-radius: 99px;
}
/* Message list — fills remaining height, scrolls from bottom */
#messages {
flex: 1;
overflow: hidden;
position: relative;
}
#msg-inner {
position: absolute;
bottom: 0; left: 0; right: 0;
display: flex;
flex-direction: column;
gap: .55rem;
padding-bottom: .2rem;
}
.msg {
display: flex;
align-items: flex-start;
gap: .55rem;
animation: slide-in .25s ease;
}
@keyframes slide-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.msg-avatar {
width: 2rem; height: 2rem;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 1rem;
flex-shrink: 0;
margin-top: .1rem;
overflow: hidden;
}
.msg-body { flex: 1; min-width: 0; }
.msg-name {
font-size: .78rem;
font-weight: 700;
margin-bottom: .15rem;
}
.msg-text {
font-size: 1.05rem;
line-height: 1.45;
word-break: break-word;
color: #e8e8e8;
}
.msg.system .msg-text {
font-size: .82rem;
color: #666;
font-style: italic;
}
.msg-img { max-width: 240px; max-height: 180px; border-radius: 8px; display: block; margin-top: .3rem; }
.msg-suggestion {
border-left: 3px solid #818cf8;
padding: .4rem .65rem;
background: #818cf818;
border-radius: 0 8px 8px 0;
margin-top: .2rem;
}
.msg-suggestion .sug-label { font-size: .72rem; color: #818cf8; margin-bottom: .15rem; }
.msg-suggestion .sug-url { font-size: .9rem; color: #e8e8e8; word-break: break-all; }
/* === QR panel === */
#qr-col {
width: 180px;
flex-shrink: 0;
background: #181818;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: .9rem;
padding: 1.2rem .8rem;
border-left: 1px solid #222;
}
#qr-canvas {
border-radius: 8px;
background: #fff;
padding: 6px;
}
#qr-label {
font-size: .72rem;
color: #666;
text-align: center;
word-break: break-all;
}
#qr-title {
font-size: .85rem;
font-weight: 600;
color: #aaa;
text-align: center;
}
.dot-live {
width: .55rem; height: .55rem;
background: #ef4444;
border-radius: 50%;
display: inline-block;
animation: pulse 1.4s infinite;
}
@keyframes pulse {
0%,100% { opacity: 1; }
50% { opacity: .3; }
}
</style>
</head>
<body>
<div id="root">
<!-- Chat column -->
<div id="chat-col">
<div id="chat-header">
<span class="dot-live"></span>
<h1>Livechat</h1>
<span id="viewer-count">0 aktiva</span>
</div>
<div id="messages">
<div id="msg-inner"></div>
</div>
</div>
<!-- QR panel -->
<div id="qr-col">
<div id="qr-title">Delta i chatten</div>
<img id="qr-img" src="/api/chat-qr" alt="QR" style="width:140px;height:140px;border-radius:8px;background:#fff;display:block">
<div id="qr-label">{{ local_ip }}:5000/chat-mobile</div>
</div>
</div>
<script>
// I overlay-läge (?overlay=1) döljs QR-panelen och bakgrunden görs transparent
const isOverlay = new URLSearchParams(location.search).has('overlay');
if (isOverlay) {
document.body.style.background = 'transparent';
document.getElementById('root').style.background = 'transparent';
document.getElementById('qr-col').style.display = 'none';
document.getElementById('chat-col').style.padding = '.5rem .75rem';
}
const MOBILE_URL = 'http://{{ local_ip }}:5000/chat-mobile';
const MAX_MSGS = 40;
// SocketIO — /chat namespace
const socket = io('/chat');
const msgInner = document.getElementById('msg-inner');
const countEl = document.getElementById('viewer-count');
function avatarBg(color) {
return `background:${color}22; color:${color};`;
}
function avatarContent(emoji, name, avatarUrl) {
if (avatarUrl) return `<img src="${avatarUrl}" style="width:100%;height:100%;object-fit:cover;border-radius:50%">`;
return escHtml(emoji || name.charAt(0).toUpperCase());
}
function addMsg(name, text, color, emoji, avatarUrl) {
const div = document.createElement('div');
div.className = 'msg';
div.innerHTML = `
<div class="msg-avatar" style="${avatarBg(color)}">${avatarContent(emoji, name, avatarUrl)}</div>
<div class="msg-body">
<div class="msg-name" style="color:${color}">${escHtml(name)}</div>
<div class="msg-text">${_renderContent(text)}</div>
</div>`;
msgInner.appendChild(div);
while (msgInner.children.length > MAX_MSGS) msgInner.removeChild(msgInner.firstChild);
}
function addSuggestion(d) {
const div = document.createElement('div');
div.className = 'msg';
div.innerHTML = `
<div class="msg-avatar" style="${avatarBg(d.color)}">${avatarContent(d.emoji, d.name, d.avatar_url)}</div>
<div class="msg-body">
<div class="msg-name" style="color:${d.color}">${escHtml(d.name)}</div>
${_suggestionHtml(d.text || d.url)}
</div>`;
msgInner.appendChild(div);
while (msgInner.children.length > MAX_MSGS) msgInner.removeChild(msgInner.firstChild);
}
function addSystem(text) {
const div = document.createElement('div');
div.className = 'msg system';
div.innerHTML = `<div class="msg-body"><div class="msg-text">${escHtml(text)}</div></div>`;
msgInner.appendChild(div);
while (msgInner.children.length > MAX_MSGS) msgInner.removeChild(msgInner.firstChild);
}
function escHtml(s) {
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}
const _IMG_RE = /^(?:https?:\/\/|\/)[^\s"<>]+\.(gif|jpe?g|png|webp|avif)(\?[^\s"<>]*)?$/i;
function _renderContent(text) {
const t = text.trim();
if (_IMG_RE.test(t)) {
const safe = t.replace(/"/g, '%22');
return `<img src="${safe}" class="msg-img" loading="lazy" onerror="this.style.display='none'">`;
}
return escHtml(text);
}
function _suggestionHtml(url) {
const short = url.length > 60 ? url.slice(0, 57) + '…' : url;
return `<div class="msg-suggestion">
<div class="sug-label">🎬 Förslag till displayen</div>
<div class="sug-url">${escHtml(short)}</div>
</div>`;
}
socket.on('chat_history', (msgs) => {
msgInner.innerHTML = '';
msgs.forEach(m => {
if (m.kind === 'play_suggestion') addSuggestion(m);
else addMsg(m.name, m.text, m.color, m.emoji, m.avatar_url);
});
});
socket.on('chat_message', (d) => {
addMsg(d.name, d.text, d.color, d.emoji, d.avatar_url);
});
socket.on('play_suggestion', (d) => {
addSuggestion(d);
});
socket.on('system_message', (d) => {
addSystem(d.text);
});
socket.on('viewer_count', (d) => {
countEl.textContent = d.count + ' aktiva';
});
</script>
</body>
</html>