1/* Foren Freebie & Laufband â Frontend. Self-contained, kein innerHTML (XSS-sicher), keine Abhängigkeiten. */ 2(function () { 3 'use strict'; 4 var CFG = window.ForenWidget; 5 if (!CFG || !CFG.base) return; 6 7 var LS_KEY = 'fw_popup_seen'; 8 var books = null; 9 var popupShown = false; 10 var D = document; 11 12 // wp_localize_script liefert alle Skalare als STRINGS â "0" wäre in JS truthy. Schalter zu echten 13 // Booleans normalisieren, delay zu einer Zahl (sonst lieÃe sich nichts abschalten). 14 ['popup', 'exit', 'sidebar', 'laufband', 'promo', 'countdown'].forEach(function (k) { CFG[k] = CFG[k] === 1 || CFG[k] === '1'; }); 15 CFG.delay = parseInt(CFG.delay, 10); if (!(CFG.delay >= 0)) CFG.delay = 10; 16 17 /** Kleiner DOM-Helfer: erzeugt ein Element mit Klasse + reinem Text (textContent â keine XSS). */ 18 function h(tag, cls, text) { 19 var e = D.createElement(tag); 20 if (cls) e.className = cls; 21 if (text != null) e.textContent = text; 22 return e; 23 } 24 function attr(el, map) { 25 for (var k in map) if (Object.prototype.hasOwnProperty.call(map, k)) el.setAttribute(k, map[k]); 26 return el; 27 } 28 function clear(el) { while (el.firstChild) el.removeChild(el.firstChild); } 29 30 function allowed() { 31 var ex = CFG.excluded || []; 32 return (books || []).filter(function (b) { return ex.indexOf(b.id) === -1; }); 33 } 34 function pick() { 35 var a = allowed(); 36 return a.length ? a[Math.floor(Math.random() * a.length)] : null; 37 } 38 function seen() { 39 try { var v = JSON.parse(localStorage.getItem(LS_KEY) || 'null'); return v && Date.now() - v.t < v.ttl; } 40 catch (e) { return false; } 41 } 42 function markSeen(ms) { try { localStorage.setItem(LS_KEY, JSON.stringify({ t: Date.now(), ttl: ms })); } catch (e) {} } 43 function fetchBooks(cb) { 44 if (books) { cb(); return; } 45 if (CFG.books) { books = CFG.books; cb(); return; } // von WordPress serverseitig vorgecacht (12 h) â keine Plattform-Abfrage pro Besucher 46 fetch(CFG.base + '/api/forum/books').then(function (r) { return r.json(); }).then(function (d) { 47 books = (d && d.books) || []; cb(); 48 }).catch(function () { books = []; cb(); }); 49 } 50 function errText(code) { 51 var m = { invalid_email: 'Bitte eine gültige E-Mail eingeben.', disposable_email: 'Bitte keine Wegwerf-Adresse.', name_required: 'Bitte deinen Vornamen angeben.', name_too_long: 'Der Name ist zu lang.', spam_name: 'Bitte einen echten Vornamen angeben.', rate_limited: 'Zu viele Versuche â bitte kurz warten.', captcha: 'Sicherheitsprüfung fehlgeschlagen. Bitte erneut versuchen.', human_check: 'Sicherheitsfrage leider falsch. Bitte die richtige Antwort wählen.' }; 52 return m[code] || 'Anmeldung gerade nicht möglich.'; 53 } 54 55 // Rendert das Turnstile-Widget, sobald die CF-API geladen UND das Element im DOM hängt. 56 function mountTurnstile(el) { 57 var n = 0; 58 var iv = setInterval(function () { 59 if (window.turnstile && window.turnstile.render && el.isConnected) { 60 clearInterval(iv); 61 try { el.setAttribute('data-tsid', window.turnstile.render(el, { sitekey: CFG.turnstileKey })); } catch (e) {} 62 } else if (++n > 100) { clearInterval(iv); } // ~10 s Sicherheitsabbruch 63 }, 100); 64 } 65 function tsReset(form) { 66 if (!CFG.turnstileKey || !window.turnstile) return; 67 var el = form.querySelector('.fw-ts'); var id = el && el.getAttribute('data-tsid'); 68 if (id != null) try { window.turnstile.reset(id); } catch (e) {} 69 } 70 71 // ---- Dringlichkeits-Countdown: âNur heute gratis â bis 24 Uhr" mit tickender Uhr ---- 72 function pad2(n) { return (n < 10 ? '0' : '') + n; } 73 function urgencyBar() { 74 var wrap = h('div', 'fw-urg'); 75 var dateStr = new Date().toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); 76 wrap.appendChild(h('div', 'fw-urg__head', 'ð´ Achtung: Nur HEUTE gratis!')); 77 wrap.appendChild(h('div', 'fw-urg__date', 'Nur am ' + dateStr + ' â bis 24:00 Uhr')); 78 var cd = h('div', 'fw-urg__count'); 79 wrap.appendChild(cd); 80 function upd() { 81 var now = new Date(); 82 var end = new Date(); end.setHours(24, 0, 0, 0); // nächste Mitternacht (lokale Zeit) 83 var s = Math.max(0, Math.floor((end - now) / 1000)); 84 cd.textContent = 'â³ endet in ' + pad2(Math.floor(s / 3600)) + ':' + pad2(Math.floor((s % 3600) / 60)) + ':' + pad2(s % 60); 85 } 86 upd(); 87 var iv = setInterval(function () { 88 if (!cd.isConnected) { clearInterval(iv); return; } // aufräumen, wenn die Box entfernt wurde 89 upd(); 90 }, 1000); 91 return wrap; 92 } 93 94 // ---- Menschlich-Check: Bundeskanzler-Sicherheitsfrage (aktiv, solange kein Turnstile) ---- 95 // Richtige Antwort = echte(r) Kanzler(in) (Werte müssen zum Server-`isHumanAnswer` passen: kohl/merkel/merz). 96 var HUMAN_Q = [ 97 { q: 'Sicherheitsfrage: Wer war deutsche Bundeskanzlerin?', ok: { v: 'merkel', l: 'Angela Merkel' } },
98 { q: 'Sicherheitsfrage: Wer ist deutscher Bundeskanzler (2026)?', ok: { v: 'merz', l: 'Friedrich Merz' } }, 99 { q: 'Sicherheitsfrage: Wer war deutscher Bundeskanzler?', ok: { v: 'kohl', l: 'Helmut Kohl' } } 100 ]; 101 var HUMAN_BAD = [{ v: 'maus', l: 'Micky Maus' }, { v: 'duck', l: 'Donald Duck' }, { v: 'bond', l: 'James Bond' }, { v: 'obama', l: 'Barack Obama' }]; 102 function shuffle(a) { for (var i = a.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var t = a[i]; a[i] = a[j]; a[j] = t; } return a; } 103 function humanBox() { 104 var q = HUMAN_Q[Math.floor(Math.random() * HUMAN_Q.length)]; 105 var other = shuffle(HUMAN_BAD.slice())[0]; 106 var opts = shuffle([q.ok, { v: 'trump', l: 'Donald Trump' }, other]); // Trump ist immer dabei (die offensichtlich falsche Wahl) 107 var wrap = h('div', 'fw-human'); 108 wrap.appendChild(h('label', 'fw-human__q', q.q)); 109 var sel = attr(h('select', 'fw-human__sel'), { name: 'answer', 'aria-label': q.q }); 110 var ph = h('option', null, 'Bitte wählen â¦'); ph.value = ''; sel.appendChild(ph); 111 opts.forEach(function (o) { var op = h('option', null, o.l); op.value = o.v; sel.appendChild(op); }); 112 wrap.appendChild(sel); 113 return wrap; 114 } 115 116 // ---- Freebie-Box (Sidebar + Pop-up-Inhalt) ---- 117 function freebieBox(book) { 118 var wrap = h('div', 'fw-freebie'); 119 if (CFG.countdown) wrap.appendChild(urgencyBar()); // ganz oben â in der breiten Variante volle Breite über Buch + Inhalt 120 if (book.coverUrl) { 121 var img = attr(h('img', 'fw-freebie__cover'), { src: book.coverUrl, alt: book.title || '', loading: 'lazy' }); 122 wrap.appendChild(img); 123 } 124 var body = h('div', 'fw-freebie__body'); 125 body.appendChild(h('span', 'fw-freebie__tag', 'ð Gratis')); 126 body.appendChild(h('div', 'fw-freebie__title', book.title || '')); 127 if (book.hook) body.appendChild(h('div', 'fw-freebie__hook', book.hook)); 128 var bens = (book.benefits || []).slice(0, 4); 129 if (bens.length) { 130 var ul = h('ul', 'fw-freebie__benefits'); 131 bens.forEach(function (b) { ul.appendChild(h('li', null, b)); }); 132 body.appendChild(ul); 133 } 134 var label = book.buttonLabel || 'Jetzt gratis sichern'; 135 var form = attr(h('form', 'fw-freebie__form'), { novalidate: 'novalidate' }); 136 form.setAttribute('data-t', String(Date.now())); // Zeit-Falle: Render-Zeitpunkt (zu schnelles Absenden = Bot) 137 form.appendChild(attr(h('input', 'fw-hp'), { type: 'text', name: 'website', tabindex: '-1', autocomplete: 'off', 'aria-hidden': 'true' })); 138 form.appendChild(attr(h('input', 'fw-freebie__input'), { type: 'text', name: 'name', placeholder: 'Vorname', maxlength: '20', autocomplete: 'given-name', required: 'required' })); 139 form.appendChild(attr(h('input', 'fw-freebie__input'), { type: 'email', name: 'email', placeholder: 'Deine beste E-Mail', inputmode: 'email', required: 'required' })); 140 if (CFG.turnstileKey) { var tsBox = h('div', 'fw-ts'); form.appendChild(tsBox); mountTurnstile(tsBox); } 141 else { form.appendChild(humanBox()); } // ohne Turnstile: Bundeskanzler-Sicherheitsfrage 142 form.appendChild(attr(h('button', 'fw-freebie__btn', label), { type: 'submit' })); 143 var err = h('div', 'fw-freebie__error'); err.hidden = true; form.appendChild(err); 144 form.appendChild(h('div', 'fw-freebie__fine', '100 % kostenlos · Kein Spam · Double-Opt-in')); 145 body.appendChild(form); 146 wrap.appendChild(body); 147 form.addEventListener('submit', function (e) { e.preventDefault(); submit(form, book, wrap, label, err); }); 148 return wrap; 149 } 150 151 function done(wrap, already) { 152 clear(wrap); 153 var box = h('div', 'fw-done' + (already ? ' fw-done--ready' : '')); 154 box.appendChild(h('div', 'fw-done__check', 'â ')); 155 if (already) { 156 box.appendChild(h('div', 'fw-done__head', 'Alles klar â du bist schon dabei!')); 157 box.appendChild(h('div', 'fw-done__sub', 'Dein Buch ist unterwegs. Schau in dein Postfach â falls nichts angekommen ist, bitte kurz im Spam- oder Werbung-Ordner nachsehen.')); 158 } else { 159 box.appendChild(h('div', 'fw-done__head', 'Fast geschafft â nur noch 1 Schritt!')); 160 box.appendChild(h('div', 'fw-done__sub', 'Wir haben dir gerade eine E-Mail geschickt. Bestätige sie einmal kurz â danach kommt dein Buch s
160ofort in dein Postfach.')); 161 var steps = h('ol', 'fw-done__steps'); 162 var mk = function (n, t) { var li = h('li', null); li.appendChild(h('span', 'fw-done__stepn', n)); li.appendChild(D.createTextNode(t)); return li; }; 163 steps.appendChild(mk('1', 'Ãffne dein E-Mail-Postfach')); 164 steps.appendChild(mk('2', 'Klick auf den Bestätigungs-Link in unserer E-Mail')); 165 steps.appendChild(mk('3', 'Fertig â dein Buch landet sofort bei dir')); 166 box.appendChild(steps); 167 box.appendChild(h('div', 'fw-done__hint', 'ð© Keine E-Mail gefunden? Schau im Spam- oder Werbung-Ordner nach und markiere sie als âKein Spam" â so bekommst du auch die nächsten Bücher zuverlässig.')); 168 } 169 wrap.appendChild(box); 170 } 171 172 function submit(form, book, wrap, label, err) { 173 var btn = form.querySelector('button'); 174 // Felder per querySelector holen â form.name würde die eingebaute HTMLFormElement.name treffen, nicht das Input. 175 var email = (form.querySelector('input[name="email"]').value || '').trim(); 176 var name = (form.querySelector('input[name="name"]').value || '').trim(); 177 var website = form.querySelector('input[name="website"]').value || ''; 178 if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { err.textContent = 'Bitte eine gültige E-Mail eingeben.'; err.hidden = false; return; } 179 var ms = Date.now() - Number(form.getAttribute('data-t') || 0); // Zeit auf dem Formular 180 var token = ''; 181 if (CFG.turnstileKey) { 182 var tsEl = form.querySelector('.fw-ts'); var id = tsEl && tsEl.getAttribute('data-tsid'); 183 if (window.turnstile && id != null) { try { token = window.turnstile.getResponse(id) || ''; } catch (e) {} } 184 if (!token) { err.textContent = 'Bitte kurz die Sicherheitsprüfung abschlieÃen.'; err.hidden = false; return; } 185 } 186 var answer = ''; 187 var sel = form.querySelector('select[name="answer"]'); 188 if (sel) { answer = sel.value; if (!answer) { err.textContent = 'Bitte die Sicherheitsfrage beantworten.'; err.hidden = false; return; } } 189 err.hidden = true; btn.disabled = true; btn.textContent = 'â¦'; 190 fetch(CFG.base + '/api/forum/lead', { 191 method: 'POST', 192 headers: { 'content-type': 'application/json', 'x-forum-key': CFG.key }, 193 body: JSON.stringify({ email: email, name: name, leadMagnetId: book.id, forumDomain: CFG.forumDomain, website: website, ms: ms, turnstileToken: token, answer: answer }) 194 }).then(function (r) { return r.json(); }).then(function (d) { 195 if (d && d.ok) { done(wrap, d.already); markSeen(365 * 864e5); } 196 else { err.textContent = errText(d && d.error); err.hidden = false; btn.disabled = false; btn.textContent = label; tsReset(form); } 197 }).catch(function () { err.textContent = 'Etwas ist schiefgelaufen. Bitte später erneut.'; err.hidden = false; btn.disabled = false; btn.textContent = label; tsReset(form); }); 198 } 199 200 // ---- Sidebar ----
201 function renderSidebar() { 202 var slots = D.querySelectorAll('[data-fw-freebie]'); 203 Array.prototype.forEach.call(slots, function (slot) { 204 if (slot.getAttribute('data-fw-done')) return; 205 var b = pick(); if (!b) return; 206 slot.setAttribute('data-fw-done', '1'); 207 var box = freebieBox(b); 208 if (slot.hasAttribute('data-fw-wide')) box.className += ' fw-freebie--wide'; // volle Breite unter Beiträgen/im Forum 209 slot.appendChild(box); 210 }); 211 } 212 213 // ---- Pop-up (Timer + Exit-Intent) ---- 214 function showPopup() { 215 if (popupShown || seen()) return; 216 var b = pick(); if (!b) return; 217 popupShown = true; markSeen(1 * 864e5); // 24 Std Sperre nach Anzeige/Wegklicken (Anmelder: 1 Jahr, s. submit) 218 var root = h('div', 'fw-pop'); 219 var backdrop = h('div', 'fw-pop__backdrop'); 220 var card = attr(h('div', 'fw-pop__card'), { role: 'dialog', 'aria-modal': 'true' }); 221 var closeBtn = attr(h('button', 'fw-pop__close', 'Ã'), { 'aria-label': 'SchlieÃen', type: 'button' }); 222 var inner = h('div', 'fw-pop__inner'); 223 inner.appendChild(freebieBox(b)); 224 card.appendChild(closeBtn); card.appendChild(inner); 225 root.appendChild(backdrop); root.appendChild(card); 226 function close() { root.remove(); D.removeEventListener('keydown', onKey); } 227 function onKey(e) { if (e.key === 'Escape') close(); } 228 closeBtn.addEventListener('click', close); 229 backdrop.addEventListener('click', close); 230 D.addEventListener('keydown', onKey); 231 D.body.appendChild(root); 232 } 233 function setupPopup() { 234 if (CFG.popup) setTimeout(showPopup, Math.max(0, CFG.delay) * 1000); 235 if (CFG.exit) { 236 var armed = false; 237 setTimeout(function () { armed = true; }, 4000); 238 D.addEventListener('mouseout', function (e) { if (armed && e.clientY <= 0) showPopup(); }); 239 } 240 } 241 242 // ---- Laufband ---- 243 function annItem(it) { 244 var node; 245 if (it.link) { node = attr(h('a', 'fw-annbar__item'), { href: it.link, target: '_blank', rel: 'noopener' }); } 246 else { node = h('span', 'fw-annbar__item'); } 247 if (it.icon) node.appendChild(h('span', 'fw-annbar__ic', it.icon)); 248 node.appendChild(D.createTextNode(it.text)); 249 return node; 250 } 251 function renderLaufband() { 252 var build = function (items) { 253 if (!items || !items.length) return; 254 var track = h('div', 'fw-annbar__track'); 255 for (var r = 0; r < 2; r++) { // zwei identische Reihen â nahtloses Endlos-Laufband (translateX -50%) 256 var rowEl = h('div', 'fw-annbar__row'); 257 for (var i = 0; i < 8; i++) items.forEach(function (it) { rowEl.appendChild(annItem(it)); }); 258 track.appendChild(rowEl); 259 } 260 var vp = h('div', 'fw-annbar__vp'); vp.appendChild(track); 261 var bar = h('div', 'fw-annbar'); bar.appendChild(vp); 262 D.body.insertBefore(bar, D.body.firstChild); 263 // Ruhiges, konstantes Tempo: Dauer aus der Breite EINER Reihe (~40 px/s), egal wie viele Meldungen. 264 var row1 = track.querySelector('.fw-annbar__row'); 265 if (row1 && row1.offsetWidth) track.style.animationDuration = Math.max(35, Math.round(row1.offsetWidth / 40)) + 's'; 266 }; 267 if (CFG.announcements) { build(CFG.announcements); return; } // von WordPress serverseitig vorgecacht (12 h) 268 fetch(CFG.base + '/api/forum/announcements?lang=de').then(function (r) { return r.json(); }).then(function (d) { build((d && d.items) || []); }).catch(function () {}); 269 } 270 271 // ---- Schwebendes Werbebild (Sticky Promo) ---- 272 var PROMO_POS = { 'right-middle': 1, 'left-middle': 1, 'bottom-right': 1, 'bottom-left': 1, 'top-right': 1, 'top-left': 1 }; 273 var PROMO_SIZE = { small: 1, medium: 1, large: 1, xlarge: 1 }; 274 function buildPromo(p) { 275 if (!p || !p.imageUrl || !p.url) return; // keins gesetzt â nichts anzeigen 276 try { if (sessionStorage.getItem('fw_promo_dismiss') === String(p.id)) return; } catch (e) {} // in dieser Sitzung weggeklickt 277 var pos = PROMO_POS[p.position] ? p.position : 'right-middle'; 278 var size = PROMO_SIZE[p.size] ? p.size : 'medium'; 279 var box = h('div', 'fw-promo fw-promo--' + pos + ' fw-promo--size-' + size); 280 var a = attr(h('a', 'fw-promo__link'), { href: p.url, rel: 'sponsored noopener', target: '_blank' }); // immer neuer Tab 281 var img = attr(h('img', 'fw-promo__img'), { alt: p.alt || '' }); // KEIN loading=lazy â sofort laden (fixe Position wird sonst evtl. nie geladen) 282 a.appendChild(img); 283 box.appendChild(a);
284 var x = attr(h('button', 'fw-promo__x', 'Ã'), { type: 'button', 'aria-label': 'Werbung ausblenden', title: 'Ausblenden' }); 285 x.addEventListener('click', function () { 286 try { sessionStorage.setItem('fw_promo_dismiss', String(p.id)); } catch (e) {} 287 box.parentNode && box.parentNode.removeChild(box); 288 }); 289 box.appendChild(x); 290 // Box ERST einhängen, wenn das Bild geladen ist â kein 0-Höhe-Kollaps und kein leerer Kasten, 291 // falls das Bild langsam lädt oder von einem Adblocker geblockt wird. 292 var shown = false; 293 var reveal = function () { if (shown) return; shown = true; D.body.appendChild(box); }; 294 img.onload = reveal; 295 img.onerror = function () {}; // geblockt/kaputt â gar nicht anzeigen 296 img.src = p.imageUrl; // src ZULETZT setzen, damit onload zuverlässig feuert 297 if (img.complete && img.naturalWidth > 0) reveal(); // sofort, wenn schon im Cache 298 } 299 function renderPromo() { 300 if ('promoData' in CFG) { buildPromo(CFG.promoData); return; } // serverseitig vorgeladen (1 h) â kein Fetch pro Besucher 301 fetch(CFG.base + '/api/forum/promo').then(function (r) { return r.json(); }).then(function (d) { buildPromo(d && d.promo); }).catch(function () {}); 302 } 303 304 function init() { 305 if (CFG.laufband) renderLaufband(); 306 if (CFG.promo) renderPromo(); 307 fetchBooks(function () { 308 renderSidebar(); // füllt ALLE vorhandenen [data-fw-freebie]-Slots (Sidebar, unter Beiträgen, im Forum) 309 setupPopup(); 310 }); 311 } 312 if (D.readyState === 'loading') D.addEventListener('DOMContentLoaded', init); else init(); 313})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.