// The hunter's board: missions in their own order, a live timer while they are on the site, a // code box when they are back. One open mission at a time. (() => { const $ = id => document.getElementById(id); const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); const api = async (path, body) => { const r = await fetch(path, body ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) } : {}); const j = await r.json().catch(() => ({})); if (r.status === 401) location.href = '/?signin=1'; return j; }; let board = null, open = null; // open: { missionId, token, url, dwell, started } function card(m) { const isOpen = open && open.missionId === m.id; const r = m.reward || {}; return '
' + '' + (m.done ? 'Completed' : esc(m.site)) + '' + '

' + esc(m.name) + '

' + esc(m.brief) + '

' + '

' + r.minPol + ' to ' + r.maxPol + ' POL · ' + m.dwell + 's on the site

' + (m.done ? '' : isOpen ? '
Your code appears on the site after ' + m.dwell + 's. Keep that tab open.
' + '
' + '
' + '

Open the site again ↗

' : '
') + '
'; } function render() { $('who').innerHTML = ' ' + esc(board.me.username ? '@' + board.me.username : '#' + board.me.memberId) + (board.me.wallet ? '' : ' · no wallet linked'); const r = board.missions[0] && board.missions[0].reward; if (r) $('rangeLine').textContent = 'Each find pays a random drip between ' + r.minPol + ' and ' + r.maxPol + ' POL. Your codes are yours alone.'; $('missions').innerHTML = board.missions.length ? board.missions.map(card).join('') : '

No missions are open right now. Check back soon.

'; $('mine').innerHTML = board.drips.length ? board.drips.map(d => '
' + esc(d.site) + '' + ({ paid: 'paid', sent: 'sending', due: 'paying next', queued: 'queued for the next pool', failed: 'held, being looked at' }[d.status] || d.status) + '+' + d.pol + ' POL' + (d.tx ? 'verify ↗' : '') + '
').join('') : '
Nothing yet. Your first find goes here.
'; document.querySelectorAll('[data-start]').forEach(b => b.addEventListener('click', () => start(b.dataset.start))); const s = $('submit'); if (s) { s.addEventListener('click', submit); $('code').addEventListener('keydown', e => { if (e.key === 'Enter') submit(); }); tick(); } } let explorer = 'https://polygonscan.com'; async function load() { board = await api('/api/my/board'); render(); } async function start(id) { const r = await api('/api/my/start', { missionId: id }); if (r.error) { alert(r.error); return; } open = { missionId: id, token: r.token, url: r.url, dwell: r.dwell, started: Date.now(), expires: r.expires }; window.open(r.url, '_blank', 'noopener'); render(); } function tick() { const el = $('timer'); if (!el || !open) return; const left = Math.max(0, open.dwell - Math.floor((Date.now() - open.started) / 1000)); el.innerHTML = left > 0 ? 'Your code appears on the site in about ' + left + 's. Stay on the site tab until it shows (switching away pauses it), then come back and type it here.' : 'Your code should be showing on the site now. If it is not, switch to the site tab and give it a few seconds. Then type it here.'; if (open.expires && Date.now() > open.expires) { el.innerHTML = 'This attempt timed out. Start the mission again.'; return; } setTimeout(tick, 1000); } async function submit() { const code = $('code').value.trim(); if (!code) return; $('submit').disabled = true; const r = await api('/api/my/submit', { token: open.token, code }); const m = $('msg'); m.className = 'msg ' + (r.error ? 'bad' : 'ok'); m.textContent = r.error || r.message; $('submit').disabled = false; if (!r.error) { open = null; setTimeout(load, 900); } } fetch('/api/config').then(r => r.json()).then(c => { explorer = c.explorer || explorer; }).catch(() => {}); load(); })();