Files
polhunter/public/embed.js
T

52 lines
5.0 KiB
JavaScript

/* PolHunter embed. One line on a mission site:
<script src="https://polhunter.com/embed.js" data-slots="3" async></script>
plus, where a code may appear, elements with data-ph-slot="0" ... data-ph-slot="N-1".
When a hunter arrives with ?ph=<token> (their own, from their PolHunter board) the token is kept
in sessionStorage for this tab, and after the mission's dwell the code is fetched from
polhunter.com and rendered in ONE of the slots (which one is picked per hunter per day). The
request carries this page's Origin; polhunter.com answers only for the mission's own host and
only after the dwell. Anyone else loading this page sees nothing at all. */
(function () {
var API = 'https://polhunter.com';
try {
// the token arrives in the hash (#ph=) or, for older links, the query (?ph=); it is kept for this tab
var h = new URLSearchParams((location.hash || '').replace(/^#/, '')), q = new URLSearchParams(location.search), t = h.get('ph') || q.get('ph');
if (t) { sessionStorage.setItem('ph.token', t); h.delete('ph'); q.delete('ph'); history.replaceState(null, '', location.pathname + (q.toString() ? '?' + q : '') + (h.toString() ? '#' + h : '')); }
t = sessionStorage.getItem('ph.token'); if (!t) return;
} catch (e) { return; }
// slots: explicit [data-ph-slot="i"] elements, or the i-th match of data-selector (e.g. ".video-card"
// on a training page, ".card" on a programs page), else a fixed badge in the corner
var me = document.currentScript, slots = Number((me && me.getAttribute('data-slots')) || 1) || 1, sel = me && me.getAttribute('data-selector');
function slotEl(i) {
var el = document.querySelector('[data-ph-slot="' + i + '"]'); if (el) return el;
if (sel) { var all = document.querySelectorAll(sel); if (all.length) { var host = all[i % all.length]; var d0 = document.createElement('div'); d0.style.cssText = 'margin:10px 0'; host.appendChild(d0); return d0; } }
var d = document.createElement('div'); d.setAttribute('data-ph-slot', String(i)); d.style.cssText = 'position:fixed;right:16px;bottom:16px;z-index:99999'; document.body.appendChild(d); return d;
}
function render(code, slot) {
var el = slotEl(slot); el.innerHTML = '';
var box = document.createElement('div');
box.setAttribute('role', 'note');
// the code as six fixed cells on its own row: a short read is visible at a glance, nothing can wrap or clip
// a single glyph away, and a tap copies it for the board (OPG saw 5 of 6 characters twice, 2026-09-20)
box.style.cssText = 'display:inline-flex;flex-direction:column;align-items:flex-start;gap:8px;padding:12px 14px;border-radius:14px;background:linear-gradient(135deg,#1a0f3d,#2b1a63);color:#f1eefb;font:600 13px/1.2 system-ui,-apple-system,"Segoe UI",sans-serif;box-shadow:0 8px 30px rgba(130,71,229,.45),0 0 0 1px rgba(255,255,255,.08);letter-spacing:.02em;max-width:100%;cursor:pointer;-webkit-user-select:none;user-select:none';
var cells = code.split('').map(function (ch) { return '<span style="display:inline-block;min-width:26px;padding:5px 0;text-align:center;border-radius:7px;background:rgba(0,0,0,.35);border:1px solid rgba(243,190,67,.45);font:700 18px/1 ui-monospace,Menlo,monospace;color:#f3be43">' + ch + '</span>'; }).join('');
box.innerHTML = '<span style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;max-width:100%"><img src="' + API + '/img/coin-sm.png" alt="" width="26" height="26" style="width:26px;height:26px;object-fit:contain;flex:none;filter:drop-shadow(0 0 8px rgba(130,71,229,.85))"><span style="white-space:nowrap">PolHunter code</span> <span style="opacity:.7;font-weight:500;font-size:12px">6 characters, tap to copy</span></span><span style="display:inline-flex;gap:4px;white-space:nowrap">' + cells + '</span>';
box.setAttribute('title', 'Tap to copy ' + code);
box.addEventListener('click', function () { var done = function () { var s = box.querySelector('span span'); if (s) s.textContent = 'copied, paste it on your board'; }; try { navigator.clipboard.writeText(code).then(done, done); } catch (e) { done(); } });
el.appendChild(box);
}
var tries = 0, pending = null, done = false;
// phones freeze a background tab's timers: when the hunter comes back to this tab, ask right away
document.addEventListener('visibilitychange', function () { if (document.visibilityState === 'visible' && !done) { clearTimeout(pending); ask(); } });
function ask() {
tries++; if (tries > 80 || done) return;
fetch(API + '/api/embed/code?t=' + encodeURIComponent(t), { mode: 'cors', credentials: 'omit' }).then(function (r) { return r.json(); }).then(function (r) {
if (r && r.code) { done = true; render(r.code, Number(r.slot) % slots); try { sessionStorage.removeItem('ph.token'); } catch (e) {} return; }
if (r && r.wait) { pending = setTimeout(ask, Math.min(r.wait, 5) * 1000); return; }
// expired / gone / wrong origin: say nothing
}).catch(function () { pending = setTimeout(ask, 6000); });
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', ask); else ask();
})();