3594e78d4a
- Artikel-Link zeigt „→ Dialog · N" wenn Wortmeldungen existieren - Widget lädt alle 10s nach, rendert nur bei Änderung neu (kein Flackern), pausiert im Hintergrund-Tab. Echtes Supabase-Realtime bleibt optional. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
170 lines
8.3 KiB
JavaScript
170 lines
8.3 KiB
JavaScript
/* OPENBUREAU Dialog — flache Wortmeldungen pro Beitrag.
|
|
Lesen öffentlich, Mitreden nach Login (eingeladene User). Kein Build nötig. */
|
|
(function () {
|
|
const root = document.getElementById('ob-dialog');
|
|
if (!root) return;
|
|
const thread = root.dataset.thread || new URLSearchParams(location.search).get('thread') || '';
|
|
if (!thread) { renderOverview(); return; }
|
|
|
|
// Übersicht aller begonnenen Dialoge (wenn kein Thema gewählt).
|
|
function renderOverview() {
|
|
root.innerHTML = '';
|
|
const h = document.createElement('h2'); h.className = 'dialog-title'; h.textContent = 'Dialoge';
|
|
const list = document.createElement('div'); list.className = 'dialog-overview';
|
|
root.append(h, list);
|
|
fetch('/api/threads').then((r) => (r.ok ? r.json() : [])).then((rows) => {
|
|
if (!rows.length) {
|
|
const e = document.createElement('p'); e.className = 'dialog-empty';
|
|
e.textContent = 'Noch keine Dialoge begonnen.';
|
|
list.appendChild(e); return;
|
|
}
|
|
rows.forEach((t) => {
|
|
const a = document.createElement('a'); a.className = 'dialog-overview-item';
|
|
a.href = '/dialog/?thread=' + encodeURIComponent(t.thread);
|
|
const ti = document.createElement('span'); ti.className = 'dialog-ov-title'; ti.textContent = t.title;
|
|
const mt = document.createElement('span'); mt.className = 'dialog-ov-meta';
|
|
mt.textContent = t.count + (t.count === 1 ? ' Wortmeldung' : ' Wortmeldungen');
|
|
a.append(ti, mt); list.appendChild(a);
|
|
});
|
|
}).catch(() => {});
|
|
}
|
|
const TKEY = 'ob_dialog_token', NKEY = 'ob_dialog_name';
|
|
let token = localStorage.getItem(TKEY);
|
|
let myName = localStorage.getItem(NKEY);
|
|
let replyTo = null;
|
|
let textarea = null;
|
|
|
|
root.innerHTML = '';
|
|
const title = document.createElement('h2');
|
|
title.className = 'dialog-title'; title.textContent = 'Dialog';
|
|
const list = document.createElement('div'); list.className = 'dialog-list';
|
|
const composer = document.createElement('div'); composer.className = 'dialog-composer';
|
|
root.append(title, list, composer);
|
|
|
|
function fmt(ts) {
|
|
const d = new Date(ts), s = (Date.now() - d) / 1000;
|
|
if (s < 60) return 'gerade eben';
|
|
if (s < 3600) return Math.floor(s / 60) + ' Min.';
|
|
if (s < 86400) return Math.floor(s / 3600) + ' Std.';
|
|
return d.toLocaleDateString('de-CH');
|
|
}
|
|
|
|
let lastSig = '';
|
|
async function load() {
|
|
let data = [];
|
|
try {
|
|
const res = await fetch('/api/comments?thread=' + encodeURIComponent(thread));
|
|
if (res.ok) data = await res.json();
|
|
} catch { return; /* offline: alte Ansicht behalten */ }
|
|
// Nur neu rendern, wenn sich wirklich etwas geändert hat (kein Flackern).
|
|
const sig = (token ? 'in:' : 'out:') + data.map((c) => c.id + (c.deleted ? 'd' : '')).join(',');
|
|
if (sig !== lastSig) { lastSig = sig; render(data); }
|
|
}
|
|
|
|
function render(data) {
|
|
list.innerHTML = '';
|
|
const names = {}; data.forEach((c) => { names[c.id] = c.author_name; });
|
|
if (!data.length) {
|
|
const e = document.createElement('p'); e.className = 'dialog-empty';
|
|
e.textContent = 'Noch keine Wortmeldungen — beginne den Dialog.';
|
|
list.appendChild(e); return;
|
|
}
|
|
data.forEach((c) => {
|
|
const card = document.createElement('article'); card.className = 'dialog-card';
|
|
const head = document.createElement('header'); head.className = 'dialog-card-head';
|
|
const av = document.createElement('span'); av.className = 'dialog-avatar';
|
|
if (c.author_avatar) av.style.backgroundImage = 'url(' + c.author_avatar + ')';
|
|
else av.textContent = (c.author_name || '?').slice(0, 1).toUpperCase();
|
|
const meta = document.createElement('div'); meta.className = 'dialog-meta';
|
|
const nm = document.createElement('span'); nm.className = 'dialog-name'; nm.textContent = c.author_name || 'Unbekannt';
|
|
const tm = document.createElement('time'); tm.className = 'dialog-time'; tm.textContent = fmt(c.created_at);
|
|
meta.append(nm, tm);
|
|
if (c.parent_id && names[c.parent_id]) {
|
|
const rp = document.createElement('span'); rp.className = 'dialog-replyto'; rp.textContent = '↳ ' + names[c.parent_id];
|
|
meta.appendChild(rp);
|
|
}
|
|
head.append(av, meta); card.appendChild(head);
|
|
const bd = document.createElement('div'); bd.className = 'dialog-body'; bd.textContent = c.body; card.appendChild(bd);
|
|
if (token && !c.deleted) {
|
|
const actions = document.createElement('div'); actions.className = 'dialog-actions';
|
|
const rep = document.createElement('button'); rep.textContent = 'Antworten';
|
|
rep.onclick = () => { replyTo = { id: c.id, name: c.author_name }; renderComposer(); if (textarea) textarea.focus(); };
|
|
const del = document.createElement('button'); del.textContent = 'Löschen'; del.onclick = () => remove(c.id);
|
|
actions.append(rep, del); card.appendChild(actions);
|
|
}
|
|
list.appendChild(card);
|
|
});
|
|
}
|
|
|
|
function renderComposer() {
|
|
composer.innerHTML = '';
|
|
if (token) {
|
|
if (replyTo) {
|
|
const r = document.createElement('button'); r.className = 'dialog-replychip';
|
|
r.textContent = 'Antwort auf ' + replyTo.name + ' ✕';
|
|
r.onclick = () => { replyTo = null; renderComposer(); };
|
|
composer.appendChild(r);
|
|
}
|
|
textarea = document.createElement('textarea'); textarea.className = 'dialog-textarea';
|
|
textarea.placeholder = 'Deine Wortmeldung …';
|
|
const row = document.createElement('div'); row.className = 'dialog-row';
|
|
const send = document.createElement('button'); send.className = 'dialog-send'; send.textContent = 'Senden'; send.onclick = submit;
|
|
const out = document.createElement('button'); out.className = 'dialog-logout'; out.textContent = 'Abmelden' + (myName ? ' · ' + myName : ''); out.onclick = logout;
|
|
row.append(send, out); composer.append(textarea, row);
|
|
} else {
|
|
const hint = document.createElement('p'); hint.className = 'dialog-loginhint'; hint.textContent = 'Zum Mitreden anmelden:';
|
|
const em = document.createElement('input'); em.type = 'email'; em.placeholder = 'E-Mail'; em.className = 'dialog-input';
|
|
const pw = document.createElement('input'); pw.type = 'password'; pw.placeholder = 'Passwort'; pw.className = 'dialog-input';
|
|
const btn = document.createElement('button'); btn.className = 'dialog-send'; btn.textContent = 'Anmelden';
|
|
btn.onclick = () => doLogin(em.value, pw.value);
|
|
pw.onkeydown = (e) => { if (e.key === 'Enter') doLogin(em.value, pw.value); };
|
|
composer.append(hint, em, pw, btn);
|
|
}
|
|
}
|
|
|
|
async function doLogin(email, password) {
|
|
try {
|
|
const res = await fetch('/api/auth/login', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email, password }),
|
|
});
|
|
const j = await res.json().catch(() => ({}));
|
|
if (!res.ok) { alert(j.error || 'Login fehlgeschlagen'); return; }
|
|
token = j.access_token; myName = j.name || '';
|
|
localStorage.setItem(TKEY, token); localStorage.setItem(NKEY, myName);
|
|
renderComposer(); load();
|
|
} catch { alert('Login fehlgeschlagen'); }
|
|
}
|
|
|
|
function logout() {
|
|
token = null; myName = null; replyTo = null;
|
|
localStorage.removeItem(TKEY); localStorage.removeItem(NKEY);
|
|
renderComposer(); load();
|
|
}
|
|
|
|
async function submit() {
|
|
const body = textarea.value.trim(); if (!body) return;
|
|
const res = await fetch('/api/comments', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
|
|
body: JSON.stringify({ thread, body, parent_id: replyTo ? replyTo.id : null }),
|
|
});
|
|
if (res.status === 401) { logout(); alert('Sitzung abgelaufen — bitte neu anmelden.'); return; }
|
|
const j = await res.json().catch(() => ({}));
|
|
if (!res.ok) { alert(j.error || 'Senden fehlgeschlagen'); return; }
|
|
textarea.value = ''; replyTo = null; renderComposer(); load();
|
|
}
|
|
|
|
async function remove(id) {
|
|
if (!confirm('Wortmeldung löschen?')) return;
|
|
const res = await fetch('/api/comments/' + id, { method: 'DELETE', headers: { Authorization: 'Bearer ' + token } });
|
|
if (!res.ok) { const j = await res.json().catch(() => ({})); alert(j.error || 'Löschen fehlgeschlagen'); return; }
|
|
load();
|
|
}
|
|
|
|
renderComposer();
|
|
load();
|
|
// Live genug: alle 10 s nachladen (pausiert, wenn der Tab im Hintergrund ist).
|
|
setInterval(() => { if (!document.hidden) load(); }, 10000);
|
|
})();
|