Launch build: k=3 floor-weighted draw, three missions per hunter per day, HUNT_LIVE_AT lifts the curtain and opens outbound at the launch moment, badge photos to topic + general, toasts on the board
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Admin · PolHunter</title>
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
<link rel="stylesheet" href="/style.css?v=11">
|
||||
<link rel="stylesheet" href="/style.css?v=12">
|
||||
<style>
|
||||
label{display:block;font-size:12px;color:var(--dim);letter-spacing:.08em;text-transform:uppercase;margin:10px 0 4px}
|
||||
input,textarea,select{width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--edge);background:rgba(0,0,0,.35);color:var(--ink);font:14px var(--font)}
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Your board · PolHunter</title>
|
||||
<meta name="robots" content="noindex">
|
||||
<link rel="stylesheet" href="/style.css?v=11">
|
||||
<link rel="stylesheet" href="/style.css?v=12">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
@@ -46,6 +46,6 @@
|
||||
<footer class="foot">Rewards are for completed missions, not income. The daily pool is limited; when it is spent, claims close until midnight Central. Cryptocurrency involves risk of loss.</footer>
|
||||
</div>
|
||||
<div class="modal" id="shareModal" hidden><div class="modal-card"><button class="modal-x" id="shareClose" type="button" aria-label="Close">×</button><div id="shareBody"></div></div></div>
|
||||
<script src="/app.js?v=7"></script>
|
||||
<script src="/app.js?v=8"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+27
-3
@@ -5,6 +5,27 @@
|
||||
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 }
|
||||
// toasts: small, stacked, gone in a few seconds
|
||||
function toast(msg, kind, link) {
|
||||
let host = $('toasts'); if (!host) { host = document.createElement('div'); host.id = 'toasts'; document.body.appendChild(host); }
|
||||
const t = document.createElement('div'); t.className = 'toast ' + (kind || ''); t.innerHTML = msg + (link ? ' <a href="' + esc(link.href) + '" target="_blank" rel="noopener">' + esc(link.text) + '</a>' : '');
|
||||
host.appendChild(t); requestAnimationFrame(() => t.classList.add('in'));
|
||||
setTimeout(() => { t.classList.remove('in'); setTimeout(() => t.remove(), 400); }, kind === 'big' ? 9000 : 6000);
|
||||
}
|
||||
const seenPaid = new Set(); let seenLedger = null;
|
||||
// a drip that flips to paid while the board is open gets a toast with its proof
|
||||
function watchPaid() {
|
||||
for (const d of (board.drips || [])) if (d.status === 'paid' && d.tx) { if (seenPaid.size && !seenPaid.has(d.id)) toast('\u{1F4B0} <b>' + d.pol + ' POL</b> landed in your wallet.', 'ok', { href: explorer + '/tx/' + d.tx, text: 'verify \u2197' }); seenPaid.add(d.id); }
|
||||
if (!seenPaid.size) for (const d of (board.drips || [])) if (d.status === 'paid') seenPaid.add(d.id);
|
||||
}
|
||||
// other hunters' finds, as they happen
|
||||
async function watchLedger() {
|
||||
try { const r = await (await fetch('/api/ledger')).json(); const rows = r.recent || [];
|
||||
if (seenLedger) { const fresh = rows.filter(x => !seenLedger.has(x.tx)).slice(0, 2); for (const x of fresh) if (!board || !board.me || x.who !== (board.me.username ? '@' + board.me.username : '#' + board.me.memberId)) toast('\u{1F3AF} ' + esc(x.who) + ' just found <b>' + x.pol + ' POL</b> on ' + esc(x.site) + '.', ''); }
|
||||
seenLedger = new Set(rows.map(x => x.tx));
|
||||
} catch (e) {}
|
||||
}
|
||||
setInterval(watchLedger, 45000); setInterval(async () => { if (board && (board.drips || []).some(d => d.status === 'due' || d.status === 'sent')) { await load(); } }, 30000);
|
||||
|
||||
function card(m) {
|
||||
const isOpen = open && open.missionId === m.id;
|
||||
@@ -13,7 +34,7 @@
|
||||
+ '<span class="tag' + (m.done ? ' done' : '') + '">' + (m.done ? 'Completed' : esc(m.site)) + '</span>'
|
||||
+ '<h3>' + esc(m.name) + '</h3><p>' + esc(m.brief) + '</p>'
|
||||
+ '<p style="margin-top:10px"><span class="range">' + r.minPol + ' to ' + r.maxPol + ' POL</span> <span style="color:var(--dim);font-size:12px">· ' + m.dwell + 's on the site</span></p>'
|
||||
+ (m.done ? '' : (board.pool && board.pool.spent && !isOpen) ? '<div style="margin-top:14px"><span class="tag gold">Closed until midnight Central</span></div>' : isOpen
|
||||
+ (m.done ? '' : (board.pool && board.pool.spent && !isOpen) ? '<div style="margin-top:14px"><span class="tag gold">Closed until midnight Central</span></div>' : (board.daily && !board.daily.left && !isOpen) ? '<div style="margin-top:14px"><span class="tag">Back tomorrow</span></div>' : isOpen
|
||||
? '<div class="timer" id="timer">Your code appears on the site after <b>' + m.dwell + 's</b>. Keep that tab open.</div>'
|
||||
+ '<div class="codebox"><input id="code" maxlength="6" placeholder="CODE" autocomplete="off" spellcheck="false"><button class="btn pol" id="submit">Claim</button></div>'
|
||||
+ '<div class="msg" id="msg"></div>'
|
||||
@@ -78,6 +99,8 @@
|
||||
const copy = async (el, btn) => { try { await navigator.clipboard.writeText(el.value); } catch (e) { el.select(); document.execCommand('copy'); } const t = btn.textContent; btn.textContent = 'Copied'; setTimeout(() => { btn.textContent = t; }, 1200); };
|
||||
$('copylink').onclick = () => copy($('mylink'), $('copylink')); $('copytext').onclick = () => copy($('sharetext'), $('copytext'));
|
||||
}
|
||||
watchPaid(); if (seenLedger === null) watchLedger();
|
||||
const dl = board.daily; if (dl && $('rangeLine')) $('rangeLine').innerHTML = ($('rangeLine').textContent.replace(/\s*·\s*You have.*$/, '')) + ' \u00b7 ' + (dl.left ? 'You have <b>' + dl.left + ' of ' + dl.limit + '</b> missions left today.' : '<b>That is your ' + dl.limit + ' for today.</b> Fresh missions at midnight Central.');
|
||||
$('mine').innerHTML = board.drips.length ? board.drips.map(d => '<div class="row"><span>' + esc(d.site) + '</span><span class="site">' + ({ paid: 'paid', sent: 'sending', due: 'paying next', queued: 'queued for the next pool', failed: 'held, being looked at' }[d.status] || d.status) + '</span><span class="pol">+' + d.pol + ' POL</span>' + (d.tx ? '<a class="when" target="_blank" rel="noopener" href="' + explorer + '/tx/' + d.tx + '">verify ↗</a>' : '<span class="when"></span>') + '</div>').join('')
|
||||
: '<div class="row"><span class="site">Nothing yet. Your first find goes here.</span></div>';
|
||||
document.querySelectorAll('[data-start]').forEach(b => b.addEventListener('click', () => start(b.dataset.start)));
|
||||
@@ -87,7 +110,7 @@
|
||||
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) { if (r.spent) { await load(); } else alert(r.error); return; }
|
||||
if (r.error) { toast(esc(r.error), 'bad'); if (r.spent || r.dailyDone) await load(); 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();
|
||||
@@ -104,9 +127,10 @@
|
||||
$('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;
|
||||
if (r.error) toast(esc(r.error), 'bad'); else toast('\u{1F389} <b>Found it!</b> ' + r.pol + ' POL is on its way to your wallet.', 'big');
|
||||
if (!r.error) { const a = document.createElement('a'); a.href = '#share'; a.textContent = ' Share this find \u2197'; a.style.marginLeft = '6px'; m.appendChild(a); delete $('sharetext').dataset.touched; }
|
||||
// an achievement unlocked by this find: celebrate it once the board has reloaded
|
||||
if (!r.error && r.unlocked && r.unlocked.length) { const id = r.unlocked[0]; setTimeout(async () => { await load(); shareBadge(id, true); }, 1200); }
|
||||
if (!r.error && r.unlocked && r.unlocked.length) { const id = r.unlocked[0]; toast('\u{1F3C6} <b>Achievement unlocked!</b>', 'big'); setTimeout(async () => { await load(); shareBadge(id, true); }, 1200); }
|
||||
$('submit').disabled = false;
|
||||
if (!r.error || r.spent) { open = null; setTimeout(load, 900); }
|
||||
}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
<meta property="og:url" content="https://polhunter.com/">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:image" content="https://polhunter.com/promo/polhunter-1200x630.jpg">
|
||||
<link rel="stylesheet" href="/style.css?v=11">
|
||||
<link rel="stylesheet" href="/style.css?v=12">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Leaderboard · PolHunter</title>
|
||||
<meta name="description" content="The hunters with the most finds this week, this month and all time. Every drip is on Polygon.">
|
||||
<link rel="stylesheet" href="/style.css?v=11">
|
||||
<link rel="stylesheet" href="/style.css?v=12">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Promo tools · PolHunter</title>
|
||||
<meta name="description" content="Banners, posts, swipes and the teaser video, all carrying your InstantAdPay referral.">
|
||||
<link rel="stylesheet" href="/style.css?v=11">
|
||||
<link rel="stylesheet" href="/style.css?v=12">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
@@ -55,6 +55,11 @@ textarea{width:100%;padding:12px 14px;border-radius:12px;border:1px solid var(--
|
||||
.sharebtns{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px} .share .card textarea{margin-top:10px}
|
||||
.banners{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px} .banner{background:var(--glass);border:1px solid var(--edge);border-radius:18px;padding:16px} .banner .bhead{margin-bottom:10px} .banner .bimg{padding:6px 0} .banner .bimg img{max-width:100%;height:auto;display:block;border-radius:6px} .banner textarea{margin-top:10px;font:500 12px var(--mono)}
|
||||
.small{font-size:13px;color:var(--dim)}
|
||||
/* toasts */
|
||||
#toasts{position:fixed;right:16px;bottom:16px;z-index:1200;display:flex;flex-direction:column;gap:8px;max-width:min(360px,calc(100vw - 32px));pointer-events:none}
|
||||
.toast{pointer-events:auto;padding:12px 16px;border-radius:14px;background:linear-gradient(135deg,#1a0f3d,#2b1a63);border:1px solid rgba(180,140,255,.45);color:var(--ink);font-size:14px;box-shadow:0 12px 40px rgba(130,71,229,.35);opacity:0;transform:translateY(12px);transition:opacity .3s,transform .3s}
|
||||
.toast.in{opacity:1;transform:none} .toast.ok{border-color:rgba(75,227,165,.5)} .toast.bad{border-color:rgba(255,122,122,.55)} .toast.big{font-size:16px;border-color:var(--gold);box-shadow:0 12px 40px rgba(243,190,67,.35)} .toast a{color:var(--gold-hi)}
|
||||
@media (max-width:600px){#toasts{left:16px;right:16px;bottom:12px;max-width:none}}
|
||||
/* badge cards */
|
||||
.badge-strip{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:14px}
|
||||
.badge-a{background:var(--glass);border:1px solid var(--edge);border-radius:18px;padding:12px;text-align:center;transition:border-color .2s,transform .2s}
|
||||
|
||||
Reference in New Issue
Block a user