PageSourceSearch

https://online-dating-forum.de/wp-content/plugins/foren-widget/assets/widget.js?ver=1.5.0

js online-dating-forum.de collected 2026-09-24 11:09:42 UTC 18,133 bytes, 313 lines download raw bytes

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.