184ca99902
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
52 lines
5.0 KiB
JavaScript
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 data-ph-hint 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('[data-ph-hint]'); 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();
|
|
})();
|