1/* w3-report.js â "Report a problem" button for the fleet's static game sites. 2 * 3 * Self-contained (no CSS or framework needed): a small pill fixed bottom-left that 4 * opens a panel with the reasons the w3admin Game Reports queue understands and an 5 * optional note, and POSTs to https://w3admin.w3technic.com/api/report.php â the 6 * same endpoint the WordPress portals and the gamesite-core ReportGame component 7 * use. Rendered in a shadow root so the host page's CSS cannot touch it and vice 8 * versa. Injected by w3-agents/report-widget/inject.py; the gamesite-core sites 9 * have their own button in the player bar and are skipped by the injector. 10 * 11 * Navbar mode: if the page has an element with `data-w3-report-slot`, the button is 12 * mounted inside it as a plain nav link (inherits the bar's colour/font) and the 13 * panel drops down below it instead of floating bottom-left. 14 * 15 * Slug = last path segment of the page ("home" on /), game name = the page's H1 16 * (falling back to <title>); the page URL and the browser go along with it. 17 */ 18(function () { 19 if (window.__w3Report || window.top !== window.self) return; // never inside a game frame 20 window.__w3Report = true; 21 var API = 'https://w3admin.w3technic.com/api/report.php'; 22 var REASONS = [ 23 ['not_loading', "Won't load"], 24 ['blank_screen', 'Black / blank screen'], 25 ['broken_controls', 'Controls do not work'], 26 ['wrong_game', 'Wrong game'], 27 ['slow', 'Too slow / laggy'], 28 ['other', 'Something else'], 29 ]; 30 function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); } 31 function slug() { 32 var p = location.pathname.replace(/\/+$/, '').replace(/\.html?$/, ''); 33 var s = p.split('/').filter(Boolean).pop() || 'home'; 34 return s.toLowerCase().replace(/[^a-z0-9/-]+/g, '-').slice(0, 191) || 'home'; 35 } 36 function gameName() { 37 var h = document.querySelector('h1'); 38 var t = (h && h.textContent.trim()) || document.title || location.hostname; 39 return t.replace(/\s+/g, ' ').slice(0, 120); 40 } 41 42 var host = document.createElement('div'); 43 host.id = 'w3-report'; 44 host.setAttribute('data-w3-report', '1'); 45 var root = host.attachShadow ? host.attachShadow({ mode: 'open' }) : host; 46 root.innerHTML = 47 '<style>' + 48 ':host{all:initial;position:fixed;left:14px;bottom:14px;z-index:2147483000;font:13px/1.35 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;color:#111}' + 49 '@media(max-width:640px){:host{bottom:76px;left:10px}}' + 50 '@media print{:host{display:none}}' + 51 ':host(.inline){position:relative;left:auto;bottom:auto;display:inline-block;z-index:2147483000;font:inherit;color:inherit}' + 52 ':host(.inline) .pill{height:auto;padding:0;border-radius:0;background:none;box-shadow:none;backdrop-filter:none;color:inherit;font:inherit;opacity:1;gap:5px}' + 53 ':host(.inline) .pill:hover{text-decoration:underline}' + 54 ':host(.inline) .panel{bottom:auto;top:calc(100% + 12px);left:-8px}' + 55 '.pill{display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 12px 0 10px;border:0;border-radius:999px;background:rgba(17,17,17,.78);color:#fff;font:600 12px/1 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;cursor:pointer;box-shadow:0 2px 10px rgba(0,0,0,.25);backdrop-filter:blur(4px);opacity:.85}' + 56 '.pill:hover{opacity:1}.pill svg{flex:none}' + 57 '.panel{position:absolute;left:0;bottom:40px;width:min(300px,calc(100vw - 28px));padding:12px;border-radius:12px;background:#fff;color:#111;box-shadow:0 8px 30px rgba(0,0,0,.28);border:1px solid #e3e3e3;display:none}' + 58 '.panel.open{display:block}' + 59 '.h{margin:0 0 8px;font-weight:700;font-size:13px}' + 60 '.r{display:flex;flex-wrap:wrap;gap:6px}' + 61 '.r button{border:1px solid #d9d9d9;background:#f4f4f4;color:#444;border-radius:999px;padding:6px 10px;font:500 12px/1 inherit;cursor:pointer}' + 62 '.r button[aria-pressed=true]{background:#1d6ee0;border-color:#1d6ee0;color:#fff}' + 63 'textarea{display:block;width:100%;box-sizing:border-box;margin-top:8px;padding:6px 8px;border:1px solid #d9d9d9;border-radius:8px;font:13px/1.35 inherit;resize:none;color:#111;background:#fff}' + 64 '.f{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:8px}' + 65 '.f small{color:#666;font-size:11px}' + 66 '.send{flex:none;border:0;border-radius:8px;background:#1d6ee0;color:#fff;font:700 12px/1 inherit;padding:8px 12px;cursor:pointer}' + 67 '.send[disabled]{opacity:.6}' + 68 '.done p{margin:0 0 4px}.done .x{margin-top:8px;border:0;border-radius:8px;background:#eee;color:#111;font:600 12px/1 inherit;padding:7px 10px;cursor:pointer}' + 69 '</style>' + 70 '<button class="pill" type="button" aria-expanded="false" aria-label="Report a problem with this game" title="Report a problem">' + 71 '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 22V4a1 1 0 0 1 1-1h11l-1 4 1 4H5"/></svg>
71' + 72 '<span>Report a problem</span></button>' + 73 '<div class="panel" role="dialog" aria-label="Report a problem"></div>'; 74 75 var pill = root.querySelector('.pill'), panel = root.querySelector('.panel'); 76 var reason = 'not_loading', note = '', state = 'idle'; 77 78 function render() { 79 var name = gameName(); 80 if (state === 'sent') { 81 panel.innerHTML = '<div class="done"><p><strong>Thanks â report sent.</strong></p><p style="color:#666">We check every report and fix or replace the game.</p><button class="x" type="button">Close</button></div>'; 82 panel.querySelector('.x').onclick = close; 83 return; 84 } 85 var html = '<p class="h">What is wrong with ' + esc(name) + '?</p><div class="r">'; 86 REASONS.forEach(function (r) { 87 html += '<button type="button" data-k="' + r[0] + '" aria-pressed="' + (reason === r[0]) + '">' + r[1] + '</button>'; 88 }); 89 html += '</div><textarea rows="2" maxlength="500" placeholder="Anything else? (optional)">' + esc(note) + '</textarea>' + 90 '<div class="f"><small>' + (state === 'error' ? 'Could not send â try again in a minute.' : 'Sends the page URL and your browser.') + '</small>' + 91 '<button class="send" type="button"' + (state === 'sending' ? ' disabled' : '') + '>' + (state === 'sending' ? 'Sendingâ¦' : 'Send report') + '</button></div>'; 92 panel.innerHTML = html; 93 panel.querySelectorAll('.r button').forEach(function (b) { 94 b.onclick = function () { reason = b.getAttribute('data-k'); render(); }; 95 }); 96 panel.querySelector('textarea').oninput = function (e) { note = e.target.value; }; 97 panel.querySelector('.send').onclick = send; 98 } 99 function open() { panel.classList.add('open'); pill.setAttribute('aria-expanded', 'true'); render(); } 100 function close() { panel.classList.remove('open'); pill.setAttribute('aria-expanded', 'false'); } 101 function send() { 102 if (state === 'sending') return; 103 state = 'sending'; render(); 104 var body = { 105 slug: slug(), game_name: gameName(), source: location.hostname, page_url: location.href.slice(0, 500), reason: reason, 106 note: (note.trim() ? note.trim().slice(0, 500) + ' · ' : '') + 'UA: ' + navigator.userAgent.
106slice(0, 200), 107 }; 108 fetch(API, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) 109 .then(function (r) { return r.json(); }) 110 .then(function (d) { state = d && d.ok ? 'sent' : 'error'; render(); }) 111 .catch(function () { state = 'error'; render(); }); 112 } 113 pill.onclick = function () { panel.classList.contains('open') ? close() : open(); }; 114 document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); }); 115 document.addEventListener('mousedown', function (e) { if (panel.classList.contains('open') && e.target !== host) close(); }); 116 if (document.querySelector('[data-w3-report-slot]')) pill.querySelector('span').textContent = 'Report'; 117 118 var slotEl = document.querySelector('[data-w3-report-slot]'); 119 if (slotEl) { 120 host.classList.add('inline'); slotEl.appendChild(host); 121 // nav items usually carry a small z-index (Cookie Clicker: #topBar>div = 1), which 122 // would trap the drop-down under the page; lift the slot itself. 123 if (!slotEl.style.zIndex) { slotEl.style.zIndex = '2147483000'; if (getComputedStyle(slotEl).position === 'static') slotEl.style.position = 'relative'; } 124 } 125 else (document.body || document.documentElement).appendChild(host); 126})();
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.