/* ===================================================================== DIE SPENDENKARTE (28.09.2026) Was im Bild erscheint, wenn jemand etwas gegeben hat: Betrag, Name, Gruß — und ein Bild dazu. --------------------------------------------------------------------- WARUM DAS EIN EIGENES STÜCK IST UND NICHT TEIL DER REACTION Dieselbe Karte läuft an zwei Orten: in der Reaction-Seite, wo alle zusehen, und später als eigene Seite für OBS und TikTok Studio. Zweimal gebaut hieße: Bei der nächsten Änderung sieht sie an einem der beiden Orte anders aus — und man merkt es erst im Livestream. Deshalb hängt hier nichts an der Reaction. Wer die Karte zeigen will, gibt ihr einen Kasten und eine Karte, sonst nichts. --------------------------------------------------------------------- EINE NACH DER ANDEREN Zwei Spenden gleichzeitig sind keine Seltenheit — wenn es im Live gut läuft, kommen drei in zehn Sekunden. Übereinander gelegt wäre keine mehr lesbar, und ausgerechnet die größte ginge unter. Also eine Schlange: Was ankommt, wartet, bis die vorige durch ist. DIE SCHLANGE HAT EINE GRENZE. Bei zwanzig wartenden Karten stünde die Anzeige zwei Minuten hinter dem Geschehen; wer dann noch spendet, sieht seine Karte, wenn die Sendung vorbei ist. Ab dann werden die Wartezeiten gekürzt statt die Karten weggeworfen — jede kommt dran, nur schneller. ===================================================================== */ (() => { 'use strict'; const el = (art, klasse, text) => { const n = document.createElement(art); if (klasse) n.className = klasse; if (text !== undefined) n.textContent = text; return n; }; /* Die mitgelieferten Vorlagen. Gezeichnet und nicht als Bilddatei: Sie müssen in jeder Farbe funktionieren (die Stufe bringt ihre eigene mit), und eine Zeichnung nimmt jede an. */ const VORLAGEN = { herz: '', welle: '' + '', krone: '', stern: '', pfote: '' + '', }; function zeichenBauen(name, farbe) { const inhalt = VORLAGEN[name] || VORLAGEN.herz; const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('viewBox', '0 0 64 64'); svg.setAttribute('aria-hidden', 'true'); svg.setAttribute('fill', farbe); svg.setAttribute('stroke', farbe); svg.innerHTML = inhalt; return svg; } /** Betrag aus Cent in einen lesbaren Text. * * GERECHNET WIRD IN CENT, FORMATIERT ERST HIER. Kommazahlen sind * bei Geld die Quelle stiller Fehler; der einzige Ort, an dem ein * Komma auftauchen darf, ist der Bildschirm. */ function alsGeld(cent, waehrung) { try { return new Intl.NumberFormat('de-DE', { style: 'currency', currency: waehrung || 'EUR', minimumFractionDigits: cent % 100 === 0 ? 0 : 2, }).format(cent / 100); } catch { return (cent / 100).toFixed(2).replace('.', ',') + ' ' + (waehrung || 'EUR'); } } const schlange = []; let laeuft = false; function naechste(kasten) { if (laeuft || !schlange.length) return; laeuft = true; const karte = schlange.shift(); /* BEI VOLLER SCHLANGE WIRD GEKUERZT, NICHT WEGGEWORFEN. Wer gespendet hat, soll seine Karte sehen -- notfalls kuerzer. */ const eile = Math.min(1, 4 / Math.max(1, schlange.length + 1)); const dauer = Math.max(2200, Math.round((karte.stufe?.dauer_ms || 7000) * eile)); const tafel = bauen(karte); kasten.append(tafel); /* Ein Bild in derselben Umdrehung anzuhaengen und gleich zu bewegen, ueberspringt der Browser -- er fasst beides zusammen. Ein Bildaufbau dazwischen, dann laeuft es. */ requestAnimationFrame(() => requestAnimationFrame(() => { tafel.dataset.da = 'ja'; })); setTimeout(() => { tafel.dataset.da = 'nein'; setTimeout(() => { tafel.remove(); laeuft = false; naechste(kasten); }, 600); }, dauer); } function bauen(karte) { const farbe = karte.stufe?.farbe || '#8ec9ff'; const tafel = el('div', 'spendenkarte'); tafel.style.setProperty('--sp-farbe', farbe); /* DIE GROESSE KOMMT VON DER STUFE. Genau das ist „spezielle sachen bei speziellen spenden": Eine Rudel-Legende darf größer dastehen als ein Danke. BEGRENZT, obwohl der Server schon prüft -- diese Datei läuft auch auf der OBS-Tafel, und dort kommt der Wert über den Bühnenstrom. Eine Zahl, die aus der Leitung kommt, wird nicht ungeprüft in ein Stilblatt geschrieben. */ const g = Number(karte.stufe?.groesse); tafel.style.setProperty('--sp-gross', String(Number.isFinite(g) && g >= 1 && g <= 2.5 ? g : 1)); tafel.dataset.stufe = karte.stufe?.name || ''; tafel.setAttribute('role', 'status'); const bild = el('div', 'spendenkarte__bild'); if (karte.stufe?.bild) { /* Ein eigenes Bild schlaegt die Vorlage -- und wenn es nicht laedt, tritt die Vorlage wieder ein. Ein leerer Kasten waere das Schlimmste von beidem. */ const b = document.createElement('img'); b.src = karte.stufe.bild; b.alt = ''; b.addEventListener('error', () => { b.remove(); bild.append(zeichenBauen(karte.stufe?.vorlage, farbe)); }); bild.append(b); } else { bild.append(zeichenBauen(karte.stufe?.vorlage, farbe)); } tafel.append(bild); const text = el('div', 'spendenkarte__text'); text.append(el('span', 'spendenkarte__wer', karte.name || 'Jemand')); text.append(el('strong', 'spendenkarte__betrag', alsGeld(karte.betrag_cent, karte.waehrung))); if (karte.stufe?.name) { text.append(el('span', 'spendenkarte__stufe', karte.stufe.name)); } if (karte.gruss) text.append(el('p', 'spendenkarte__gruss', karte.gruss)); tafel.append(text); return tafel; } /** Eine Karte zeigen. Kasten und Karte, sonst nichts. */ function zeigen(kasten, karte) { if (!kasten || !karte) return; /* DIESELBE KARTE NICHT ZWEIMAL. Der Ereignisstrom wird bei einer Stoerung wiederholt; ohne diese Sperre liefe eine Spende doppelt, und zwar ausgerechnet dann, wenn das Netz hakt. */ if (schlange.some((k) => k.id && k.id === karte.id)) return; schlange.push(karte); /* HOECHSTENS ZWANZIG WARTEN. Darueber ist die Anzeige nicht mehr Begleitung, sondern Rueckstand. */ while (schlange.length > 20) schlange.shift(); naechste(kasten); } window.Spendenkarte = { zeigen, alsGeld, VORLAGEN: Object.keys(VORLAGEN) }; })();