1/* iChallenge Arena v5 client. No framework. All user/game text is inserted with textContent. */ 2(function () { 3 "use strict"; 4 var bootEl = document.getElementById("boot"); 5 if (!bootEl) return; 6 var BOOT = JSON.parse(bootEl.textContent); 7 var REDUCED = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; 8 var LS = { 9 get: function (k) { try { return localStorage.getItem(k); } catch (e) { return null; } }, 10 set: function (k, v) { try { localStorage.setItem(k, v); } catch (e) {} }, 11 del: function (k) { try { localStorage.removeItem(k); } catch (e) {} }, 12 }; 13 var TOKEN_KEY = BOOT.preview ? "ic_t_preview" : "ic_t"; 14 var state = { player: null, token: LS.get(TOKEN_KEY) }; 15 16 // ---------- helpers ---------- 17 function $(s, r) { return (r || document).querySelector(s); } 18 function $$(s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); } 19 function h(tag, attrs) { 20 var el = document.createElement(tag); 21 if (attrs) for (var k in attrs) { 22 if (attrs[k] == null) continue; 23 if (k === "class") el.className = attrs[k]; 24 else if (k === "text") el.textContent = attrs[k]; 25 else if (k.slice(0, 2) === "on") el.addEventListener(k.slice(2), attrs[k]); 26 else el.setAttribute(k, attrs[k]); 27 } 28 for (var i = 2; i < arguments.length; i++) { 29 var c = arguments[i]; 30 if (c == null || c === false) continue; 31 if (Array.isArray(c)) { c.forEach(function (x) { if (x) el.appendChild(x); }); continue; } 32 el.appendChild(typeof c === "string" || typeof c === "number" ? document.createTextNode(String(c)) : c); 33 } 34 return el; 35 } 36 function svgEl(markup) { 37 var t = document.createElement("template"); 38 t.innerHTML = markup; // static, code-owned markup only 39 return t.content.firstChild; 40 } 41 function fmt(n) { return Number(n || 0).toLocaleString("en-US"); } 42 function withArena(path) { 43 if (!BOOT.preview) return path; 44 var hash = ""; 45 var i = path.indexOf("#"); 46 if (i > -1) { hash = path.slice(i); path = path.slice(0, i); } 47 return path + (path.indexOf("?") > -1 ? "&" : "?") + "arena=" + encodeURIComponent(BOOT.host) + hash; 48 } 49 50 function api(path, body, method) { 51 var headers = { "content-type": "application/json" }; 52 if (state.token) headers.authorization = "Bearer " + state.token; 53 if (BOOT.preview) headers["x-arena-host"] = BOOT.host; 54 return fetch(path, { method: method || (body ? "POST" : "GET"), headers: headers, body: body ? JSON.stringify(body) : undefined, credentials: "omit" }) 55 .then(function (r) { 56 return r.json().catch(function () { return {}; }).then(function (j) { j._status = r.status; return j; }); 57 }) 58 .catch(function () { return { error: "You're offline. Check your connection and try again.", _status: 0 }; }); 59 } 60 61 function toast(title, sub, badgeId) { 62 var box = $("#toasts"); 63 if (!box) return; 64 var t = h("div", { class: "toast" }); 65 if (badgeId) t.appendChild(svgEl(badgeSvg(badgeId))); 66 t.appendChild(h("div", null, h("b", { text: title }), sub ? h("span", { text: sub }) : null)); 67 box.appendChild(t); 68 setTimeout(function () { t.remove(); }, 4200); 69 } 70 71 var GLYPHS = { 72 first_round: "M8 4h8M12 4v16M8 20h8", flawless: "M5 12.5l4.5 4.5L19 7.5", lightning: "M13 3L5 13.5h6L10 21l9-11h-6z", 73 on_a_roll: "M12 3c1 3.5 5 5.5 5 10a5 5 0 01-10 0c0-2.5 1.5-4 2.5-5.5.5 2 1.5 3 2.5 3 0-2.5-1-5 0-7.5z", 74 week_warrior: "M4 6h16v14H4zM4 10h16M8 3v5M16 3v5", unbreakable: "M12 3l7 3v6c0 4.5-3 7.5-7 9-4-1.5-7-4.5-7-9V6z", 75 explorer: "M12 21a9 9 0 110-18 9 9 0 010 18zM15.5 8.5l-2 5-5 2 2-5z", 76 globetrotter: "M12 21a9 9 0 110-18 9 9 0 010 18zM3 12h18M12 3c2.5 2.5 3.5 5.5 3.5 9s-1 6.5-3.5 9c-2.5-2.5-3.5-5.5-3.5-9s1-6.5 3.5-9z", 77 polymath: "M4 5.5C6.5 4 9.5 4 12 5.5 14.5 4 17.5 4 20 5.5V19c-2.5-1.5-5.5-1.5-8 0-2.5-1.5-5.5-1.5-8 0zM12 5.5V19", 78 in_the_arena: "M5 21V4M5 4h11l-2 4 2 4H5", finisher: "M12 15a6 6 0 100-12 6 6 0 000 12zM8.5 14L7 21l5-2.5L17 21l-1.5-7", 79 recruiter: "M9 11a3.5 3.5 0 100-7 3.5 3.5 0 000 7zM3 20c0-3.5 2.5-6 6-6s6 2.5 6 6M18 8v6M15 11h6", 80 captain: "M4 18h16M5 18L4 8l4.5 4L12 5l3.5 7L20 8l-1 10", podium: "M4 20V14h5v6M9 20V9h6v11M15 20v-8h5v8", 81 challenger: "M3 20l6.5-11 4 6 2.5-4 5 9z", saved: "M4 6h16v12H4zM4 7l8 6 8-6", 82 }; 83 function badgeSvg(id) { 84 return '<svg class="medal-svg" width="44" height="44" viewBox="0 0 48 48" aria-hidden="true"><circle cx="24" cy="24" r="22" class="medal-ring"/><circle cx="24" cy="24" r="17" class="medal-core"/><g transform="translate(12 12)">
84<path d="' + (GLYPHS[id] || GLYPHS.first_round) + '" class="medal-glyph"/></g></svg>'; 85 } 86 87 // ---------- player ---------- 88 function ensurePlayer() { 89 if (state.token && state.player) return Promise.resolve(state.player); 90 if (state.token) return loadMe().then(function (p) { return p || createPlayer(); }); 91 return createPlayer(); 92 } 93 function createPlayer() { 94 return api("/api/player", { ref: LS.get("ic_ref") || undefined, hp_field: "" }).then(function (r) { 95 if (!r.token) { toast("Couldn't start your profile", r.error || "Try again in a moment."); return null; } 96 state.token = r.token; LS.set(TOKEN_KEY, r.token); state.player = r.player; renderPlayer(); 97 return r.player; 98 }); 99 } 100 function loadMe() { 101 if (!state.token) { renderPlayer(); return Promise.resolve(null); } 102 return api("/api/me").then(function (r) { 103 if (r._status === 0) return state.player; 104 if (!r.player) { state.token = null; LS.del(TOKEN_KEY); } 105 state.player = r.player || null; 106 renderPlayer(); 107 return state.player; 108 }); 109 } 110 111 function setRing(el, frac) { 112 var c = 2 * Math.PI * 15; 113 var f = Math.max(0, Math.min(1, frac)); 114 el.style.strokeDasharray = (f * c).toFixed(1) + " " + c.toFixed(1); 115 } 116 117 function renderPlayer() { 118 var p = state.player; 119 var span = p ? Math.max(1, p.level_next - p.level_floor) : 1; 120 var frac = p ? (p.xp - p.level_floor) / span : 0; 121 $$("[data-lvl]").forEach(function (e) { e.textContent = p ? p.level : 1; }); 122 $$("[data-lvl-text]").forEach(function (e) { e.textContent = p ? p.level : 1; }); 123 $$("[data-title]").forEach(function (e) { e.textContent = p ? p.title : "Rookie"; }); 124 $$("[data-handle]").forEach(function (e) { e.textContent = p ? p.handle : "Guest"; }); 125 $$("[data-ring-fill]").forEach(function (e) { setRing(e, frac); }); 126 $$("[data-xp-bar]").forEach(function (e) { e.style.width = (frac * 100).toFixed(1) + "%"; }); 127 $$("[data-xp-text]").forEach(function (e) { e.textContent = p && p.xp ? fmt(p.xp - p.level_floor) + " / " + fmt(span) + " XP to level " + (p.level + 1) : "Play a round to earn XP"; }); 128 var streak = p && p.streak_alive ? p.streak : 0; 129 $$("[data-streak]").forEach(function (e) { e.textContent = streak; }); 130 $$("[data-streak-text]").forEach(function (e) { e.textContent = streak; }); 131 $$("[data-streak-sub]").forEach(function (e) { e.textContent = !p ? "days" : p.played_today ? "days · safe today" : streak ? "days · play today" : "days"; }); 132 $$("[data-streak-chip]").forEach(function (e) { e.classList.toggle("is-hot", !!(p && p.played_today)); }); 133 $$("[data-league]").forEach(function (e) { var l = p ? p.league : "bronze"; e.textContent = l.charAt(0).toUpperCase() + l.slice(1); e.setAttribute("data-l", l); }); 134 var owned = {}; 135 if (p && p.badges) p.badges.forEach(function (b) { owned[b.id] = true; }); 136 $$("[data-badge]").forEach(function (e) { e.classList.toggle("is-locked", !owned[e.getAttribute("data-badge")]); }); 137 if (p && p.email_saved) { 138 $$(".save").forEach(function (s) { s.classList.add("is-saved"); }); 139 var note = $("[data-save-note]"); 140 if (note && !note.textContent) note.textContent = p.email_verified ? "Your profile is saved." : "Email saved."; 141 } 142 var profile = $("[data-profile]"); 143 if (profile && p) { 144 var set = function (sel, v) { var e = $(sel, profile); if (e) e.textContent = v; }; 145 set("[data-xp-total]", fmt(p.xp)); set("[data-best-streak]", p.best_streak); set("[data-rounds]", p.rounds_played); 146 set("[data-perfect]", p.perfect_rounds); set("[data-freezes]", p.freezes); 147 var link = $("[data-invite-link]", profile); 148 if (link) link.textContent = location.origin + "/?ref=" + p.ref_code; 149 } 150 } 151 152 // ---------- round engine ---------- 153 var round = null; 154 var timerRAF = 0; 155 var roundEl = $("[data-round]"); 156 157 function showPane(name) { 158 if (!roundEl) return; 159 ["idle", "live", "done"].forEach(function (n) { $("[data-round-" + n + "]", roundEl).hidden = n !== name; }); 160 } 161 162 function refreshToday() { 163 if (!roundEl) return; 164 api("/api/round/today").then(function (t) { 165 if (t._status !== 200) return; 166 var pt = $("[data-players-today]", roundEl); if (pt) pt.textContent = fmt(t.players_today); 167 var daily = $("[data-start='daily']", roundEl), practice = $("[data-start='practice']", roundEl), note = $("[data-round-note]", roundEl); 168 if (t.mine && t.mine.status === "done") { 169 daily.textContent = "See today's result"; daily.dataset.showResult = "1"; 170 practice.hidden = false;
171 note.textContent = "You scored " + fmt(t.mine.score) + " today (" + t.mine.correct + "/" + t.mine.total + "). Next round in " + hm(t.resets_in_ms) + "."; 172 state.todayMine = t.mine; 173 } else if (t.mine && t.mine.status === "open") { 174 daily.textContent = "Resume today's round"; 175 } 176 }); 177 } 178 function hm(ms) { var m = Math.max(0, Math.round(ms / 60000)); return Math.floor(m / 60) + "h " + (m % 60) + "m"; } 179 180 function start(mode) { 181 var btn = $("[data-start='" + mode + "']", roundEl); 182 if (btn && btn.dataset.showResult && state.todayMine) return showSavedResult(state.todayMine); 183 if (btn) btn.disabled = true; 184 ensurePlayer().then(function (p) { 185 if (!p) { if (btn) btn.disabled = false; return null; } 186 return api("/api/round/start", { mode: mode }).then(function (r) { 187 if (btn) btn.disabled = false; 188 if (r._status === 409 && r.today && r.today.mine) { state.todayMine = r.today.mine; return showSavedResult(r.today.mine); } 189 if (!r.round_id) { toast("Couldn't start the round", r.error || "Try again."); return; } 190 round = { id: r.round_id, mode: mode, score: r.score || 0, combo: r.combo || 0, results: [], total: r.question.total, locked: false }; 191 showPane("live"); 192 renderQuestion(r.question, r.elapsed_ms || 0); 193 roundEl.scrollIntoView({ behavior: REDUCED ? "auto" : "smooth", block: "nearest" }); 194 }); 195 }); 196 } 197 198 function renderQuestion(q, elapsed) { 199 var live = $("[data-round-live]", roundEl); 200 live.textContent = ""; 201 var steps = h("div", { class: "q-steps", "aria-hidden": "true" }); 202 for (var i = 0; i < q.total; i++) { 203 var cls = i < round.results.length ? (round.results[i] ? "is-ok" : "is-bad") : i === q.i ? "is-on" : ""; 204 steps.appendChild(h("i", { class: cls })); 205 } 206 var C = 2 * Math.PI * 20; 207 var timer = svgEl('<div class="q-timer" aria-hidden="true"><svg viewBox="0 0 46 46" width="46" height="46"><circle cx="23" cy="23" r="20" class="t-track"/><circle cx="23" cy="23" r="20" class="t-fill" style="stroke-dasharray:' + C + " " + C + '"/></svg><b>20</b></div>'); 208 live.appendChild(h("div", { class: "q-head" }, steps, timer)); 209 live.appendChild(h("div", { class: "q-meta" }, 210 h("span", { text: (round.mode === "daily" ? "Daily" : "Practice") + " · Question " + (q.i + 1) + " of " + q.total }), 211 round.combo > 0 ? h("span", { class: "q-combo", text: "Combo Ã" + [1, 1.1, 1.2, 1.35, 1.5][Math.min(round.combo, 4)] }) : h("span", { class: "q-score", text: fmt(round.score) + " pts" }) 212 )); 213 live.appendChild(h("p", { class: "q-text", id: "qText", text: q.q })); 214 var opts = h("div", { class: "opts", role: "group", "aria-labelledby": "qText" }); 215 q.opts.forEach(function (o, idx) { 216 opts.appendChild(h("button", { class: "opt", type: "button", "data-choice": idx, onclick: function () { answer(idx); } }, h("kbd", { text: String(idx + 1) }), h("span", { text: o }))); 217 }); 218 live.appendChild(opts); 219 var firstBtn = $(".opt", opts); if (firstBtn && !REDUCED) firstBtn.focus({ preventScroll: true }); 220 221 var deadline = performance.now() + Math.max(0, q.time_ms - elapsed); 222 var fill = $(".t-fill", timer), label = $("b", timer); 223 cancelAnimationFrame(timerRAF); 224 (function tick() { 225 var left = deadline - performance.now(); 226 var frac = Math.max(0, left / q.time_ms); 227 fill.style.strokeDasharray = (frac * C).toFixed(1) + " " + C; 228 label.textContent = Math.max(0, Math.ceil(left / 1000)); 229 timer.classList.toggle("is-low", left < 5000); 230 if (left <= 0) { if (!round.locked) answer(-1); return; } 231 timerRAF = requestAnimationFrame(tick); 232 })(); 233 } 234 235 function answer(choice) { 236 if (!round || round.locked) return; 237 round.locked = true; 238 cancelAnimationFrame(timerRAF); 239 var live = $("[data-round-live]", roundEl); 240 $$(".opt", live).forEach(function (b) { b.disabled = true; }); 241 api("/api/round/answer", { round_id: round.id, choice: choice }).then(function (r) { 242 if (r.error) { toast("That answer didn't go through", r.error); round.locked = false; return; } 243 round.score = r.score; round.combo = r.combo; round.results.push(r.ok); 244 $$(".opt", live).forEach(function (b) { 245 var i = +b.getAttribute("data-choice"); 246 if (i === r.correct_index) b.classList.add("is-right"); 247 else if (i === choice) { b.classList.add("is-wrong"); if (!REDUCED) b.classList.add("is-shake"); } 248 else b.classList.add("is-dim"); 249 }); 250 var steps = $$(".q-steps i", live); 251 if (steps[round.results.length - 1]) steps[round.results.length - 1].className = r.ok ? "is-ok" : "is-bad"; 252 var msg = r.ok ? (r.combo > 1 ? "Correct â " + r.combo + " in a row" : "Correct") : r.timed_out ? "Time's up" : "Not quite"; 253 var reveal = h("div", { class: "reveal" + (r.ok ? "" : " is-bad"), role: "status" }, 254 h("div", null, h("b", { text: msg }), r.why ? h("div", { text: r.why }) : null),
255 h("span", { class: "pts" + (r.ok && !REDUCED ? " is-pop" : ""), text: r.ok ? "+" + r.points : "+0" })); 256 live.appendChild(reveal); 257 var go = function () { 258 if (r.done) return renderDone(r.done); 259 nextBtn.disabled = true; 260 api("/api/round/next", { round_id: round.id }).then(function (n) { 261 if (!n.question) { nextBtn.disabled = false; return toast("Couldn't load the next question", n.error || "Try again."); } 262 round.locked = false; 263 renderQuestion(n.question, n.elapsed_ms || 0); 264 }); 265 }; 266 var nextBtn = h("button", { class: "btn btn-primary q-next", type: "button", text: r.done ? "See results" : "Next question", onclick: go }); 267 live.appendChild(nextBtn); 268 nextBtn.focus({ preventScroll: true }); 269 }); 270 } 271 272 function countUp(el, to) { 273 if (REDUCED) { el.textContent = fmt(to); return; } 274 var t0 = performance.now(), dur = 900; 275 (function step(t) { 276 var k = Math.min(1, (t - t0) / dur); 277 el.textContent = fmt(Math.round(to * (1 - Math.pow(1 - k, 3)))); 278 if (k < 1) requestAnimationFrame(step); 279 })(t0); 280 } 281 282 function renderDone(d) { 283 round = null; 284 showPane("done"); 285 var box = $("[data-round-done]", roundEl); 286 box.textContent = ""; 287 box.appendChild(h("div", { class: "round-top" }, h("span", { class: "pill pill-accent", text: d.mode === "daily" ? "Daily result" : "Practice result" }), d.rank_today ? h("span", { class: "round-reset", text: "#" + d.rank_today.rank + " of " + fmt(d.rank_today.of) + " today" }) : null)); 288 if (d.level_up) box.appendChild(h("div", { class: "levelup" + (REDUCED ? "" : " is-pop") }, h("b", { text: "Level " + d.level }), h("span", { text: "You're now a " + d.title + "." }))); 289 var scoreB = h("b", { text: "0" }); 290 box.appendChild(h("div", { class: "done-score" }, scoreB, h("span", { text: "points" }))); 291 countUp(scoreB, d.score); 292 var grid = h("div", { class: "done-grid", "aria-label": d.correct + " of " + d.total + " correct" }); 293 d.grid.forEach(function (ok) { grid.appendChild(h("i", { class: ok ? "ok" : "" })); }); 294 box.appendChild(grid); 295 var rows = h("div", { class: "done-rows" }); 296 rows.appendChild(h("div", { class: "done-row" }, h("span", { text: "Correct" }), h("b", { text: d.correct + " / " + d.total }))); 297 if (d.perfect_bonus) rows.appendChild(h("div", { class: "done-row" }, h("span", { text: "Perfect round bonus" }), h("b", { text: "+" + d.perfect_bonus }))); 298 rows.appendChild(h("div", { class: "done-row" }, h("span", { text: "XP earned" }), h("b", { text: "+" + fmt(d.xp_gained) }))); 299 if (d.mode === "daily") rows.appendChild(h("div", { class: "done-row" }, h("span", { text: d.freeze_used ? "Streak (a freeze saved it)" : "Streak" }), h("b", { text: d.streak + (d.streak === 1 ? " day" : " days") }))); 300 box.appendChild(rows); 301 var span = Math.max(1, d.level_next - d.level_floor); 302 var bar = h("i", { style: "width:0%" }); 303 box.appendChild(h("div", { class: "done-xp" }, h("div", { class: "bar bar-xp" }, bar), h("p", null, h("span", { text: "Level " + d.level }), h("span", { text: fmt(d.xp_total - d.level_floor) + " / " + fmt(span) + " XP" })))); 304 requestAnimationFrame(function () { bar.style.width = (((d.xp_total - d.level_floor) / span) * 100).toFixed(1) + "%"; }); 305 if (d.badges_new && d.badges_new.length) { 306 var bl = h("div", { class: "done-badges" }); 307 d.badges_new.forEach(function (b, i) { 308 var m = h("div", { class: "medal is-new" }); 309 m.appendChild(svgEl(badgeSvg(b.id))); 310 m.appendChild(h("div", null, h("b", { text: b.name }), h("span", { text: b.desc }))); 311 bl.appendChild(m); 312 setTimeout(function () { toast("Badge unlocked: " + b.name, b.desc, b.id); }, 500 + i * 700); 313 }); 314 box.appendChild(bl); 315 } 316 var actions = h("div", { class: "done-actions" }); 317 if (d.share) actions.appendChild(h("button", { class: "btn btn-primary", type: "button", text: "Challenge a friend", onclick: function () { share(d.share); } })); 318 actions.appendChild(h("button", { class: "btn btn-secondary", type: "button", text: "Practice round", onclick: function () { start("practice"); } })); 319 if (!state.player || !state.player.email_saved) actions.appendChild(h("a", { class: "btn btn-quiet span", href: "#save", text: "Save your progress" })); 320 else if (!BOOT.hub) actions.appendChild(h("a", { class: "btn btn-quiet span", href: withArena("/#challenges"), text: "Join a challenge" })); 321 box.appendChild(actions); 322 var tgt = beatTarget(); 323 if (d.mode === "daily" && tgt && tgt.today && (!state.player || state.player.ref_code !== tgt.code)) { 324 var diff = d.score - tgt.score; 325 box.insertBefore(h("div", { class: "h2h " + (diff > 0 ? "is-win" : diff < 0 ? "is-loss" : "") }, 326 h("b", { text: diff > 0 ? "You beat " + tgt.handle + " by " + fmt(diff) : diff < 0 ? tgt.handle + " wins by " + fmt(-diff) : "Dead heat with " + tgt.handle }), 327 h("span", { text: "You " + fmt(d.score) + " · " + tgt.handle + " " + fmt(tgt.score) })), rows); 328 if (!LS.get("ic_beat_played_" + tgt.code + tgt.day)) { LS.set("ic_beat_played_" + tgt.code + tgt.day, "1"); track("beat_play"); } 329 } 330 loadMe(); 331 loadLeaderboard(); 332 if (d.mode === "daily") { 333 state.todayMine = { status: "done", score: d.score, correct: d.correct, total: d.total, grid: d.grid, xp: d.xp_gained }; 334 var daily = $("[data-start='daily']", roundEl); 335 daily.textContent = "See today's result"; daily.dataset.showResult = "1"; 336 $("[data-start='practice']", roundEl).hidden = false;
337 } 338 } 339 340 function showSavedResult(m) { 341 showPane("done"); 342 var box = $("[data-round-done]", roundEl); 343 box.textContent = ""; 344 box.appendChild(h("div", { class: "round-top" }, h("span", { class: "pill pill-accent", text: "Today's result" }))); 345 box.appendChild(h("div", { class: "done-score" }, h("b", { text: fmt(m.score) }), h("span", { text: "points" }))); 346 var grid = h("div", { class: "done-grid" }); 347 (m.grid || []).forEach(function (ok) { grid.appendChild(h("i", { class: ok ? "ok" : "" })); }); 348 box.appendChild(grid); 349 box.appendChild(h("p", { class: "round-note", text: "You've played today's ranked round on " + BOOT.brand + ". A new one opens at midnight UTC." })); 350 var gridTxt = (m.grid || []).map(function (ok) { return ok ? "ð©" : "ð¥"; }).join(""); 351 box.appendChild(h("div", { class: "done-actions" }, 352 h("button", { class: "btn btn-primary", type: "button", text: "Challenge a friend", onclick: function () { share(BOOT.brand + " · Daily #" + (BOOT.today ? BOOT.today.number : "") + "\n" + gridTxt + " " + m.correct + "/" + m.total + " · " + m.score + " pts"); } }), 353 h("button", { class: "btn btn-secondary", type: "button", text: "Practice round", onclick: function () { start("practice"); } }))); 354 } 355 356 function track(t) { 357 try { fetch("/api/event", { method: "POST", keepalive: true, headers: { "content-type": "application/json", "x-arena-host": BOOT.host }, body: JSON.stringify({ t: t }) }); } catch (e) {} 358 } 359 function beatUrl() { 360 var day = (BOOT.today && BOOT.today.day) || new Date().toISOString().slice(0, 10); 361 var q = state.player ? "?beat=" + state.player.ref_code + "&d=" + day : ""; 362 return withArena(location.origin + "/" + q); 363 } 364 // Share sheet: native share when available, otherwise one-tap links. Every share counts once toward arena stats. 365 function share(textMsg, opts) { 366 opts = opts || {}; 367 var url = opts.url || beatUrl(); 368 var msg = textMsg + (opts.url ? "" : "\nCan you beat me?"); 369 var old = $(".sharesheet"); if (old) old.remove(); 370 var counted = false; 371 var count = function () { if (!counted) { counted = true; track("share"); } }; 372 var enc = encodeURIComponent; 373 var links = [ 374 ["X", "https://twitter.com/intent/tweet?text=" + enc(msg) + "&url=" + enc(url)], 375 ["WhatsApp", "https://wa.me/?text=" + enc(msg + "\n" + url)], 376 ["LinkedIn", "https://www.linkedin.com/sharing/share-offsite/?url=" + enc(url)], 377 ["Facebook", "https://www.facebook.com/sharer/sharer.php?u=" + enc(url)], 378 ["Email", "mailto:?subject=" + enc(opts.subject || "Can you beat my score on " + BOOT.brand + "?") + "&body=" + enc(msg + "\n\n" + url)], 379 ]; 380 var sheet = h("div", { class: "sharesheet", role: "dialog", "aria-label": "Share" }, 381 h("div", { class: "sharesheet-card" }, 382 h("div", { class: "sharesheet-top" }, h("b", { text: opts.title || "Challenge a friend" }), h("button", { class: "btn btn-quiet", type: "button", text: "Close", onclick: function () { sheet.remove(); } })), 383 h("pre", { class: "sharesheet-text", text: msg + "\n" + url }), 384 h("div", { class: "sharesheet-links" }, 385 navigator.share ? h("button", { class: "btn btn-primary", type: "button", text: "Shareâ¦", onclick: function () { count(); navigator.share({ text: msg, url: url }).catch(function () {}); } }) : null, 386 h("button", { class: "btn btn-secondary", type: "button", text: "Copy", onclick: function () { count(); if (navigator.clipboard) navigator.clipboard.writeText(msg + "\n" + url).then(function () { toast("Copied", "Paste it anywhere."); }); } }), 387 links.map(function (l) { return h("a", { class: "btn btn-quiet", href: l[1], target: "_blank", rel: "noopener", text: l[0], onclick: count }); })))); 388 sheet.addEventListener("click", function (e) { if (e.target === sheet) sheet.remove(); }); 389 document.body.appendChild(sheet); 390 } 391 392 document.addEventListener("keydown", function (e) { 393 if (!round || round.locked || e.metaKey || e.ctrlKey || e.altKey) return; 394 var tag = (document.activeElement && document.activeElement.tagName) || ""; 395 if (tag === "INPUT" || tag === "TEXTAREA") return; 396 var n = parseInt(e.key, 10); 397 if (n >= 1 && n <= 4) { e.preventDefault(); answer(n - 1); } 398 }); 399 400 // ---------- leaderboard ---------- 401 function loadLeaderboard() { 402 var lb = $("[data-lb]"); 403 if (!lb) return; 404 var period = lb.getAttribute("data-period"), scope = lb.getAttribute("data-scope"); 405 api("/api/leaderboard?period=" + period + "&scope=" + scope).then(function (r) { 406 var list = $("[data-lb-list]", lb); 407 list.textContent = ""; 408 if (!r.rows || !r.rows.length) { 409 list.appendChild(h("li", { class: "lb-empty", text: period === "day" ? "No scores yet today. Play the round and take first place." : "No scores yet. Be the first on the board." })); 410 } else r.rows.forEach(function (row) { 411 var mine = state.player && row.handle === state.player.handle; 412 list.appendChild(h("li", { class: mine ? "is-me" : "" }, 413 h("span", { class: "lb-rank", text: row.rank }), 414 h("span", { class: "lb-who" }, h("b", { text: row.handle }), h("span", { text: "Level " + row.level + " · " + row.league.charAt(0).toUpperCase() + row.league.slice(1) + (row.streak > 1 ? " · " + row.streak + "-day streak" : "") })), 415 h("span", { class: "lb-xp" }, fmt(row.xp), h("small", { text: "XP" })))); 416 }); 417 var me = $("[data-lb-me]", lb); 418 if (r.me && r.me.rank > r.rows.length) { me.hidden = false; me.textContent = "You're #" + r.me.rank + " of " + fmt(r.players) + " with " + fmt(r.me.xp) + " XP"; } 419 else me.hidden = true; 420 }); 421 } 422 $$("[data-lb-period]").forEach(function (b) { 423 b.addEventListener("click", function () { 424 $$("[data-lb-period]").forEach(function (x) { x.setAttribute("aria-selected", x === b ? "true" : "false"); }); 425 $("[data-lb]").setAttribute("data-period", b.getAttribute("data-lb-period")); 426 loadLeaderboard(); 427 }); 428 }); 429 $$("[data-lb-scope]").forEach(function (b) { 430 b.addEventListener("click", function () { 431 $$("[data-lb-scope]").forEach(function (x) { x.setAttribute("aria-selected", x === b ? "true" : "false"); }); 432 $("[data-lb]").setAttribute("data-scope", b.getAttribute("data-lb-scope")); 433 loadLeaderboard(); 434 }); 435 }); 436 437 // ---------- challenges ---------- 438 function loadChallenges() { 439 var cards = $$("[data-challenge]"); 440 if (!cards.length) return; 441 api("/api/challenges").then(function (r) { 442 if (!r.challenges) return; 443 var byId = {};
444 r.challenges.forEach(function (c) { byId[c.id] = c; }); 445 cards.forEach(function (card) { renderChallenge(card, byId[card.getAttribute("data-challenge")]); }); 446 }); 447 } 448 function renderChallenge(card, c) { 449 if (!c) return; 450 var count = $("[data-ch-count]", card); 451 count.textContent = c.players ? fmt(c.players) + (c.players === 1 ? " player" : " players") + (c.finished ? " · " + fmt(c.finished) + " finished" : "") : "Be the first to join"; 452 var actions = $("[data-ch-actions]", card); 453 actions.textContent = ""; 454 var prog = $("[data-ch-progress]", card); 455 var m = c.mine; 456 card.classList.toggle("is-done", !!(m && m.status === "completed")); 457 if (!m) { 458 prog.hidden = true; 459 actions.appendChild(h("button", { class: "btn btn-secondary", type: "button", text: "Join", onclick: function () { act("/api/challenge/join", c, card); } })); 460 return; 461 } 462 if (m.status === "completed") { prog.hidden = true; actions.appendChild(h("span", { class: "ch-done", text: "Completed" })); return; } 463 if (c.kind === "streak") { 464 prog.hidden = false; 465 $("[data-ch-bar]", card).style.width = Math.min(100, (m.checkins / c.days) * 100) + "%"; 466 $("[data-ch-label]", card).textContent = "Day " + m.checkins + " of " + c.days; 467 actions.appendChild(m.checked_in_today 468 ? h("span", { class: "ch-count", text: "Checked in today" }) 469 : h("button", { class: "btn btn-primary", type: "button", text: "Check in", onclick: function () { act("/api/challenge/checkin", c, card); } })); 470 } else { 471 prog.hidden = true; 472 if (m.status === "submitted") actions.appendChild(h("span", { class: "ch-count", text: "Entry under review" })); 473 else if (m.status === "rejected") actions.appendChild(h("a", { class: "btn btn-secondary", href: withArena("/c/" + c.slug), text: "Resubmit" })); 474 else actions.appendChild(h("a", { class: "btn btn-primary", href: withArena("/c/" + c.slug), text: "Post proof" })); 475 } 476 } 477 $$("[data-scope-tab]").forEach(function (btn) { 478 btn.addEventListener("click", function () { 479 var scope = btn.getAttribute("data-scope-tab"); 480 $$("[data-scope-tab]").forEach(function (b) { b.setAttribute("aria-selected", b === btn ? "true" : "false"); }); 481 $$("[data-mission-grid] .ch").forEach(function (card) { card.hidden = scope !== "all" && card.getAttribute("data-scope") !== scope; }); 482 }); 483 }); 484 function act(path, c, card) { 485 ensurePlayer().then(function (p) { 486 if (!p) return; 487 api(path, { id: c.id }).then(function (r) { 488 if (r.error) return toast("Hmm", r.error); 489 if (r.badges_new) r.badges_new.forEach(function (b) { toast("Badge unlocked: " + b.name, b.desc, b.id); }); 490 if (path.indexOf("join") > -1) toast("You're in", c.kind === "streak" ? "Check in once a day for " + c.days + " days." : "Post your proof when you're done."); 491 if (path.indexOf("checkin") > -1 && r.status === "completed") toast("Challenge complete", "+" + c.xp_complete + " XP"); 492 else if (path.indexOf("checkin") > -1 && !r.already) toast("Checked in", "Day " + r.checkins + " of " + r.days + (r.impact ? " · +" + r.impact.amount + " " + r.impact.label + " logged" : "")); 493 loadChallenges(); loadMe(); 494 }); 495 }); 496 } 497 498 function loadGallery() { 499 var g = $("[data-gallery]"); 500 if (!g) return; 501 api("/api/challenge/gallery?id=" + encodeURIComponent(g.getAttribute("data-challenge-id"))).then(function (r) { 502 g.textContent = ""; 503 if (!r.entries || !r.entries.length) { g.appendChild(h("p", { class: "muted", text: "No entries yet. Yours could be the first." })); return; } 504 r.entries.forEach(function (e) { 505 var foot = h("div", { class: "entry-foot" }, 506 e.link ? h("a", { href: e.link, rel: "nofollow ugc noopener", target: "_blank", text: e.link_host || "View link" }) : h("span"), 507 e.status === "completed" ? h("span", { class: "ok", text: "Completed" }) : 508 h("button", { class: "btn btn-quiet", type: "button", text: "Ⲡ" + e.votes, "aria-label": "Vote for " + e.handle + "'s entry", onclick: function (ev) { 509 var btn = ev.currentTarget; 510 ensurePlayer().then(function (p) { 511 if (!p) return; 512 api("/api/challenge/vote", { entry_id: e.entry_id }).then(function (v) { 513 if (v.error) return toast("Can't vote yet", v.error); 514 btn.textContent = "Ⲡ" + v.votes; btn.disabled = true; 515 }); 516 }); 517 } })); 518 g.appendChild(h("div", { class: "entry" }, h("div", { class: "entry-top" }, h("b", { text: e.handle }), h("span", { class: "muted", text: "Level " + e.level })), e.note ? h("p", { text: e.note }) : null, foot)); 519 }); 520 }); 521 } 522 523 // ---------- forms ---------- 524 function formData(form) { 525 var o = {}; 526 $$("input,select,textarea", form).forEach(function (el) { if (el.name) o[el.name] = el.value; }); 527 return o; 528 } 529 function note(sel, msg, kind) { 530 var n = $(sel); if (!n) return; 531 n.textContent = msg; n.classList.toggle("is-bad", kind === "bad"); n.classList.toggle("is-good", kind === "good"); 532 } 533 var savedEmail = ""; 534 $$("[data-form='email']").forEach(function (form) { 535 form.addEventListener("submit", function (e) { 536 e.preventDefault(); 537 var d = formData(form); 538 if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(d.email)) return note("[data-save-note]", "Enter a valid email address.", "bad"); 539 var btn = $("button", form); btn.disabled = true; 540 ensurePlayer().then(function (p) { 541 if (!p) { btn.disabled = false; return; } 542 api("/api/player/email", d).then(function (r) { 543 btn.disabled = false;
544 if (r.error) return note("[data-save-note]", r.error, "bad"); 545 savedEmail = d.email; 546 if (r.sent) { 547 form.hidden = true; 548 var v = $("[data-form='verify']"); if (v) { v.hidden = false; $("input", v).focus(); } 549 note("[data-save-note]", "We sent a 6-digit code to " + d.email + ". Enter it to finish.", "good"); 550 } else { 551 note("[data-save-note]", r.merged_into_existing ? "That email already has a profile. We'll add sign-in codes soon so you can load it here." : "Saved. Your progress is linked to " + d.email + ".", "good"); 552 } 553 loadMe(); 554 }); 555 }); 556 }); 557 }); 558 $$("[data-form='verify']").forEach(function (form) { 559 form.addEventListener("submit", function (e) { 560 e.preventDefault(); 561 var d = formData(form); 562 api("/api/player/verify", { email: savedEmail, code: d.code }).then(function (r) { 563 if (r.error) return note("[data-save-note]", r.error, "bad"); 564 form.hidden = true; 565 note("[data-save-note]", r.merged ? "Welcome back. Your saved profile is loaded on this device." : "Your profile is saved.", "good"); 566 loadMe().then(loadLeaderboard); 567 }); 568 }); 569 }); 570 $$("[data-form='sponsor']").forEach(function (form) { 571 form.addEventListener("submit", function (e) { 572 e.preventDefault(); 573 var d = formData(form); 574 if (!d.name || !d.message || !/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(d.email)) return note("[data-sponsor-note]", "Add your name, a work email and what you'd like to sponsor.", "bad"); 575 var btn = $("button", form); btn.disabled = true; 576 api("/api/sponsor", d).then(function (r) { 577 btn.disabled = false; 578 if (r.error) return note("[data-sponsor-note]", r.error, "bad"); 579 form.reset(); 580 note("[data-sponsor-note]", "Thanks. We'll reply within two business days.", "good"); 581 }); 582 }); 583 }); 584 $$("[data-form='proof']").forEach(function (form) { 585 form.addEventListener("submit", function (e) { 586 e.preventDefault(); 587 var d = formData(form); 588 d.id = form.getAttribute("data-challenge-id"); 589 ensurePlayer().then(function (p) { 590 if (!p) return; 591 api("/api/challenge/submit", d).then(function (r) { 592 if (r.need_email) { note("[data-proof-note]", r.error, "bad"); var s = $("#saveEmail"); if (s) s.focus(); return; } 593 if (r.error) return note("[data-proof-note]", r.error, "bad"); 594 form.reset(); 595 note("[data-proof-note]", "Entry posted. It completes when approved or when it reaches 5 votes.", "good"); 596 loadGallery(); loadChallenges(); 597 }); 598 }); 599 }); 600 }); 601 $$("[data-form='partner']").forEach(function (form) { 602 form.addEventListener("submit", function (e) { 603 e.preventDefault(); 604 var d = formData(form); 605 if (!d.name || !d.message || !d.interest || !/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(d.email)) return note("[data-partner-note]", "Add your name, email, what you're interested in and a short message.", "bad"); 606 var btn = $("button", form); btn.disabled = true; 607 api("/api/partner", d).then(function (r) { 608 btn.disabled = false; 609 if (r.error) return note("[data-partner-note]", r.error, "bad"); 610 form.reset(); 611 note("[data-partner-note]", "Thanks. Your inquiry is in, and a person will reply within two business days.", "good"); 612 }); 613 }); 614 }); 615 $$("[data-share-page]").forEach(function (b) { 616 b.addEventListener("click", function () { 617 share(document.title, { url: withArena(location.origin + location.pathname), title: "Share this page", subject: document.title }); 618 }); 619 }); 620 621 // ---------- warm-up question ---------- 622 $$("[data-warmup]").forEach(function (w) { 623 var ans = +w.getAttribute("data-answer"); 624 $$("[data-wchoice]", w).forEach(function (b) { 625 b.addEventListener("click", function () { 626 if (w.classList.contains("is-answered")) return; 627 w.classList.add("is-answered"); 628 var pick = +b.getAttribute("data-wchoice");
629 $$("[data-wchoice]", w).forEach(function (x) { 630 var i = +x.getAttribute("data-wchoice"); 631 x.disabled = true; 632 x.classList.add(i === ans ? "is-right" : i === pick ? "is-wrong" : "is-dim"); 633 }); 634 var ok = pick === ans; 635 $("[data-warmup-verdict]", w).textContent = ok ? "Correct. You're warmed up." : "Not quite. The answer is " + $("[data-wchoice='" + ans + "'] span", w).textContent + "."; 636 $("[data-warmup-verdict]", w).classList.toggle("is-good", ok); 637 $("[data-warmup-after]", w).hidden = false; 638 track("warmup"); if (ok) track("warmup_ok"); 639 }); 640 }); 641 var play = $("[data-warmup-play]", w); 642 if (play) play.addEventListener("click", playFromHero); 643 }); 644 function playFromHero() { 645 if (!roundEl) { location.href = withArena("/#play"); return; } 646 roundEl.scrollIntoView({ behavior: REDUCED ? "auto" : "smooth", block: "center" }); 647 var daily = $("[data-start='daily']", roundEl); 648 if (daily && !daily.disabled && !daily.hidden) daily.click(); 649 } 650 651 652 // ---------- v6.2 Today's 3 challenges · duel · bank SportTokens ---------- 653 $$("[data-hook3]").forEach(function (root) { 654 var D; try { D = JSON.parse($("[data-hook3-data]", root).textContent); } catch (e) { return; } 655 var Q = D.qs, i = 0, ans = [], ok = 0, dots = $$(".hook3-dots i", root), body = $("[data-hook3-body]", root); 656 var P = new URLSearchParams(location.search), vs = P.has("vs") ? Math.max(0, Math.min(3, +P.get("vs") || 0)) : null, vsBy = (P.get("by") || "").slice(0, 20) || "A friend"; 657 var brand = root.getAttribute("data-brand"); 658 if (vs !== null) { 659 var vb = $("[data-hook3-vs]", root); vb.textContent = vsBy + " went " + vs + " for 3 today. Beat it."; vb.hidden = false; 660 if (!LS.get("ic_vs_seen_" + D.day)) { LS.set("ic_vs_seen_" + D.day, "1"); track("beat_view"); } 661 } 662 var played = LS.get("ic_h3_" + BOOT.host + "_" + D.day); 663 function show() { 664 var q = Q[i]; dots[i].className = "on"; body.innerHTML = ""; 665 body.appendChild(h("p", { class: "warmup-q", text: q.q })); 666 var opts = h("div", { class: "warmup-opts" }); body.appendChild(opts); 667 var why = h("p", { class: "warmup-why" }); 668 q.o.forEach(function (o, k) { 669 var b = h("button", { class: "wopt", type: "button" }, h("kbd", { text: "ABCD"[k] }), h("span", { text: o })); 670 b.addEventListener("click", function () { 671 if (ans.length > i) return; ans.push(k); var good = k === q.a; if (good) ok++; 672 dots[i].className = good ? "ok" : "no"; 673 $$(".wopt", opts).forEach(function (x, j) { x.disabled = true; x.classList.add(j === q.a ? "is-right" : j === k ? "is-wrong" : "is-dim"); }); 674 why.textContent = (good ? "Yes. " : "Nope. ") + (q.w || ("Answer: " + q.o[q.a] + ".")); 675 var nx = h("button", { class: "btn btn-primary btn-sm", type: "button", text: i < 2 ? "Next challenge â" : "See my score" }); 676 nx.addEventListener("click", function () { i++; i < 3 ? show() : done(); }); 677 body.appendChild(nx); 678 }); 679 opts.appendChild(b); 680 }); 681 body.appendChild(why); 682 } 683 function msgFor(r) { 684 var m = ok === 3 ? "Perfect. Come back tomorrow for three new ones." : ok === 2 ? "So close. Send it to a friend and see who wins." : "The challenges won today. Try a friend on them."; 685 if (vs !== null) m = (ok > vs ? "You beat " + vsBy + ". " : ok === vs ? "Tied with " + vsBy + ". " : vsBy + " still leads. ") + m; 686 if (r && r.tokens) m += " +" + r.tokens + " SportTokens" + (r.balance != null ? " (balance " + r.balance + ")" : "") + "."; 687 return m; 688 } 689 function done() { 690 body.hidden = true; var dn = $("[data-hook3-done]", root); dn.hidden = false; 691 $("[data-hook3-score]", root).textContent = ok + " for 3"; 692 LS.set("ic_h3_" + BOOT.host + "_" + D.day, String(ok)); 693 api("/api/hook", { day: D.day, answers: ans, vs: vs !== null ? 1 : 0 }).then(function (r) { 694 $("[data-hook3-msg]", root).textContent = msgFor(r); 695 if (r && r.handle) root.setAttribute("data-handle", r.handle); 696 if (r && !r.signed_in && ok > 0) $("[data-hook3-join]", root).hidden = false;
697 }); 698 } 699 var shareBtn = $("[data-hook3-share]", root); 700 if (shareBtn) shareBtn.addEventListener("click", function () { 701 var by = root.getAttribute("data-handle") || (state.player && state.player.handle) || ""; 702 var url = withArena(location.origin + "/?vs=" + ok + (by ? "&by=" + encodeURIComponent(by) : "") + "&d=" + D.day); 703 share("I went " + ok + " for 3 on today's " + brand + " challenges. Your turn:", { url: url, title: "Challenge a friend", subject: brand + " challenge" }); 704 }); 705 var playBtn = $("[data-hook3-play]", root); 706 if (playBtn) playBtn.addEventListener("click", playFromHero); 707 var jf = $("[data-hook3-join]", root); 708 if (jf) jf.addEventListener("submit", function (e) { 709 e.preventDefault(); var d = {}; new FormData(jf).forEach(function (v, k) { d[k] = v; }); d.day = D.day; d.answers = ans; 710 var btn = $("button", jf); btn.disabled = true; 711 api("/api/hook/join", d).then(function (r) { 712 btn.disabled = false; 713 if (r && r.token) { state.token = r.token; LS.set(TOKEN_KEY, r.token); } 714 if (r && r.handle) root.setAttribute("data-handle", r.handle); 715 $("[data-hook3-jm]", jf).innerHTML = r && r.balance != null ? "<b>Banked.</b> Your balance: " + r.balance + " SportTokens. <a href=\"https://sportswallet.com/?utm_source=" + BOOT.host + "\">Open your wallet â</a>" : (r && r.error) || "Thanks â you're in."; 716 if (r && r.balance != null) { $$("input", jf).forEach(function (x) { x.hidden = true; }); btn.hidden = true; } 717 }); 718 }); 719 if (played !== null && played !== undefined && played !== "") { 720 ok = +played; body.hidden = true; $$(".hook3-dots i", root).forEach(function (x) { x.className = "ok"; }); 721 $("[data-hook3-done]", root).hidden = false; $("[data-hook3-score]", root).textContent = ok + " for 3"; 722 $("[data-hook3-msg]", root).textContent = "You've played today's three. New ones tomorrow â challenge a friend in the meantime."; 723 } else show(); 724 }); 725 726 // ---------- head-to-head ---------- 727 function beatTarget() { 728 if (BOOT.beat) return BOOT.beat; 729 try { var t = JSON.parse(LS.get("ic_beat") || "null"); return t && t.day === (BOOT.today && BOOT.today.day) && t.host === BOOT.host ? t : null; } catch (e) { return null; } 730 } 731 if (BOOT.beat) { 732 var bt = BOOT.beat; 733 LS.set("ic_beat", JSON.stringify({ code: bt.code, handle: bt.handle, score: bt.score, day: bt.day, today: bt.today, host: BOOT.host })); 734 if (!LS.get("ic_ref")) LS.set("ic_ref", bt.code); 735 if (!LS.get("ic_beat_seen_" + bt.code + bt.day)) { LS.set("ic_beat_seen_" + bt.code + bt.day, "1"); track("beat_view"); } 736 } 737 $$("[data-beat-play]").forEach(function (b) { b.addEventListener("click", playFromHero); }); 738 739 $$("[data-form='handle']").forEach(function (form) { 740 form.addEventListener("submit", function (e) { 741 e.preventDefault(); 742 var d = formData(form); 743 ensurePlayer().then(function (p) { 744 if (!p) return; 745 api("/api/player/handle", d).then(function (r) { 746 if (r.error) return toast("Couldn't rename", r.error); 747 form.reset(); toast("Handle updated", d.handle); loadMe(); 748 }); 749 }); 750 }); 751 }); 752 $$("[data-copy-invite]").forEach(function (b) { 753 b.addEventListener("click", function () { 754 var t = $("[data-invite-link]").textContent; 755 if (navigator.clipboard) navigator.clipboard.writeText(t).then(function () { toast("Invite link copied", "Friends who finish a round earn you +100 XP."); }); 756 }); 757 }); 758 759 function loadHistory() { 760 var list = $("[data-history]"); 761 if (!list || !state.token) return; 762 api("/api/me/history").then(function (r) { 763 if (!r.rounds || !r.rounds.length) return; 764 list.textContent = ""; 765 r.rounds.forEach(function (x) { 766 list.appendChild(h("li", null, h("span", { text: x.domain + " · " + x.day + (x.mode === "practice" ? " · practice" : "") }), h("b", { text: x.correct + "/" + x.total }), h("b", { text: fmt(x.score) + " pts" }))); 767 }); 768 }); 769 } 770 771 // ---------- network pass: carry the profile to another arena in one click ---------- 772 document.addEventListener("click", function (e) { 773 var a = e.target.closest && e.target.closest("a[data-arena]"); 774 if (!a || !state.token || BOOT.preview || e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return; 775 e.preventDefault(); 776 var href = a.href; 777 api("/api/pass", {}).then(function (r) { 778 location.href = r.pass ? href.split("#")[0] + "#icpass=" + encodeURIComponent(r.pass) : href; 779 }); 780 }); 781 function redeemPassFromHash() { 782 var m = location.hash.match(/icpass=([^&]+)/); 783 if (!m) return Promise.resolve(); 784 history.replaceState(null, "", location.pathname + location.search);
785 return api("/api/pass/redeem", { pass: decodeURIComponent(m[1]) }).then(function (r) { 786 if (r.token) { state.token = r.token; LS.set(TOKEN_KEY, r.token); state.player = r.player; toast("Welcome, " + r.player.handle, "Your level and streak came with you."); } 787 else if (r.kept_current) toast("You're already playing here", "Save your email on both arenas to combine progress."); 788 }); 789 } 790 791 // ---------- analytics beacon (JS-executing visitors only) ---------- 792 function beacon() { 793 var key = "ic_seen_" + new Date().toISOString().slice(0, 10); 794 var first = !LS.get(key); 795 LS.set(key, "1"); 796 var payload = JSON.stringify({ u: first ? 1 : 0, p: location.pathname, r: document.referrer ? document.referrer.slice(0, 300) : "" }); 797 try { 798 fetch("/api/beacon", { method: "POST", keepalive: true, headers: { "content-type": "application/json", "x-arena-host": BOOT.host }, body: payload }); 799 } catch (e) {} 800 } 801 802 // ---------- boot ---------- 803 var ref = new URLSearchParams(location.search).get("ref"); 804 if (ref && /^[A-Za-z0-9]{6,10}$/.test(ref)) LS.set("ic_ref", ref.toUpperCase()); 805 $$("[data-start]").forEach(function (b) { b.addEventListener("click", function () { start(b.getAttribute("data-start")); }); }); 806 if (BOOT.preview) $$("a[href^='/']").forEach(function (a) { if (a.getAttribute("href").indexOf("arena=") < 0 && a.getAttribute("href").indexOf("/.well-known") < 0 && a.getAttribute("href").indexOf("/sitemap") < 0) a.setAttribute("href", withArena(a.getAttribute("href"))); }); 807 redeemPassFromHash().then(loadMe).then(function () { 808 refreshToday(); loadLeaderboard(); loadChallenges(); loadGallery(); loadHistory(); 809 if (location.hash === "#play" && roundEl) roundEl.scrollIntoView({ block: "nearest" }); 810 }); 811 beacon(); 812})();
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.