/* Campus admin panel: the main website's look and behaviour (served at /admin/home_page.js). * * - Theme: the site's light / dark / autumn and accent, read from the SAME localStorage keys the * website uses ('campus-theme', 'campus-accent'), so the panel always matches the site. * - Side drawer: the section list; on narrow screens it slides in from the cat button. * - Room editor: "View Room Details", "View Dorm" and "Edit" open it in place; the old * /admin/home/view_room/ and /admin/edit-room/ links land here with ?room=. */ (function () { 'use strict'; // ───── theme (mirrors Website/app.js) ───── var THEME_KEY = 'campus-theme', ACCENT_KEY = 'campus-accent'; var THEMES = ['light', 'dark', 'autumn']; var SUN = ''; var MOON = ''; var LEAF = ''; function store(k, v) { try { if (v === undefined) return localStorage.getItem(k); localStorage.setItem(k, v); } catch (e) { return null; } } function hexRgb(h) { h = h.replace('#', ''); return [0, 2, 4].map(function (i) { return parseInt(h.slice(i, i + 2), 16); }); } function applyTheme(t) { if (THEMES.indexOf(t) < 0) t = 'light'; document.documentElement.setAttribute('data-theme', t); syncToggles(); } function applyAccent(c) { if (!c || !/^#[0-9a-f]{6}$/i.test(c)) return; var s = document.documentElement.style, rgb = hexRgb(c); s.setProperty('--red', c); s.setProperty('--red-dark', 'rgb(' + rgb.map(function (x) { return x * 0.82 | 0; }).join(',') + ')'); s.setProperty('--red-soft', 'rgba(' + rgb.join(',') + ',0.15)'); } function current() { return document.documentElement.getAttribute('data-theme') || 'light'; } function nextTheme() { return THEMES[(THEMES.indexOf(current()) + 1) % THEMES.length]; } function syncToggles() { var n = nextTheme(); var label = { light: SUN + 'Light mode', dark: MOON + 'Dark mode', autumn: LEAF + 'Autumn mode' }[n]; var icon = { light: SUN, dark: MOON, autumn: LEAF }[n]; document.querySelectorAll('.cd-theme-toggle').forEach(function (b) { b.innerHTML = label; }); document.querySelectorAll('.theme-icon-btn').forEach(function (b) { b.innerHTML = icon; b.title = 'Switch to ' + n + ' mode'; b.setAttribute('aria-label', b.title); }); document.querySelectorAll('.cd-mode').forEach(function (b) { b.classList.toggle('active', b.dataset.theme === current()); }); } function cycleTheme() { var n = nextTheme(); store(THEME_KEY, n); applyTheme(n); } applyTheme(store(THEME_KEY) || 'light'); applyAccent(store(ACCENT_KEY)); // Another tab (the website, or a second panel tab) changed the theme: follow it. window.addEventListener('storage', function (e) { if (e.key === THEME_KEY) applyTheme(e.newValue || 'light'); if (e.key === ACCENT_KEY) applyAccent(e.newValue); }); // ───── drawer ───── function setDrawer(open) { var side = document.getElementById('adminSidebar'), scrim = document.getElementById('adminScrim'); if (side) side.classList.toggle('open', open); if (scrim) scrim.classList.toggle('open', open); } function ready(fn) { if (document.readyState !== 'loading') fn(); else document.addEventListener('DOMContentLoaded', fn); } ready(function () { syncToggles(); document.querySelectorAll('.cd-theme-toggle, .theme-icon-btn').forEach(function (b) { b.addEventListener('click', cycleTheme); }); document.querySelectorAll('.cd-mode').forEach(function (b) { b.addEventListener('click', function () { store(THEME_KEY, b.dataset.theme); applyTheme(b.dataset.theme); }); }); var burger = document.getElementById('adminBurger'); if (burger) burger.addEventListener('click', function () { var side = document.getElementById('adminSidebar'); setDrawer(!(side && side.classList.contains('open'))); }); var scrim = document.getElementById('adminScrim'); if (scrim) scrim.addEventListener('click', function () { setDrawer(false); }); // Picking a section on a phone closes the drawer. document.querySelectorAll('#adminSidebar .tab').forEach(function (t) { t.addEventListener('click', function () { if (window.innerWidth <= 900) setDrawer(false); }); }); // Room links open the editor in place instead of leaving the panel. document.addEventListener('click', function (e) { var a = e.target.closest && e.target.closest('a[href*="/admin/home/view_room/"], a[href*="/admin/edit-room/"]'); if (!a) return; var id = (a.getAttribute('href').match(/\/(\d+)\/?$/) || [])[1]; if (!id) return; e.preventDefault(); openRoomEditor(id); }); var q = new URLSearchParams(location.search).get('room'); if (q && /^\d+$/.test(q)) { openRoomEditor(q); history.replaceState(null, '', location.pathname); } }); // ───── room editor ───── function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); } var ACCESS = ['Private', 'Public', 'Unlisted', 'Dev only', 'Dev unlisted']; function modal() { var m = document.getElementById('room-editor'); if (m) return m; m = document.createElement('div'); m.id = 'room-editor'; // Its own overlay class: the panel's start-up code hides every .popup-overlay, which closed // an editor opened from a ?room= link the moment the page finished loading. m.className = 're-overlay'; m.style.cssText = 'display:none; position:fixed; inset:0; z-index:2000; align-items:center; justify-content:center; padding:16px;' + ' background:rgba(10,8,20,.5); backdrop-filter:blur(3px);'; m.innerHTML = ''; document.body.appendChild(m); m.addEventListener('click', function (e) { if (e.target === m) close(); }); m.querySelector('#re-close').addEventListener('click', close); document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && m.style.display === 'flex') close(); }); function close() { m.style.display = 'none'; } return m; } function openRoomEditor(roomId) { var m = modal(), body = m.querySelector('#re-body'); m.style.display = 'flex'; body.innerHTML = '

Loading room ' + esc(roomId) + '…

'; fetch('/admin/api/room/' + encodeURIComponent(roomId), { credentials: 'same-origin' }) .then(function (r) { return r.json(); }) .then(function (d) { if (!d.success) { body.innerHTML = '

Room ' + esc(roomId) + '

' + esc(d.message || 'Not found.') + '

'; return; } var r = d.room; var subs = (r.SubRooms || []).map(function (s) { return '' + esc(s.Name || ('subroom ' + s.SubRoomId)) + ' · ' + s.MaxPlayers + ' max'; }).join(''); body.innerHTML = '

^' + esc(r.Name) + '

' + '
' + 'id ' + r.RoomId + '' + 'by ' + (r.CreatorName ? '@' + esc(r.CreatorName) : '#' + r.CreatorAccountId) + '' + '' + r.PlayersNow + ' playing now' + (r.IsDorm ? 'dorm' : '') + (r.IsRRO ? 'Rec Room Original' : '') + subs + '
' + '' + '
' + '' + '' + '' + '' + '' + '' + '
' + '
' + '' + '' + '

' + '

Loading people…

'; body.querySelector('#re-save').addEventListener('click', function () { save(r.RoomId); }); loadPeople(r.RoomId); }) .catch(function () { body.innerHTML = '

Could not load the room.

'; }); } function save(roomId) { var f = new FormData(), $ = function (id) { return document.getElementById(id); }; f.append('roomId', roomId); f.append('name', $('re-name').value); f.append('description', $('re-desc').value); f.append('accessibility', $('re-acc').value); f.append('maxPlayers', $('re-max').value); f.append('cloningAllowed', $('re-clone').checked ? 'true' : 'false'); f.append('devRoom', $('re-dev').checked ? 'true' : 'false'); var msg = $('re-msg'), btn = $('re-save'); btn.disabled = true; msg.style.color = 'var(--muted)'; msg.textContent = 'Saving…'; fetch('/admin/api/edit_room', { method: 'POST', body: f, credentials: 'same-origin' }) .then(function (r) { return r.json(); }) .then(function (d) { msg.style.color = d.success ? 'var(--ok)' : 'var(--danger)'; msg.textContent = d.message || (d.success ? 'Saved.' : 'Failed.'); }) .catch(function () { msg.style.color = 'var(--danger)'; msg.textContent = 'Save failed.'; }) .finally(function () { btn.disabled = false; }); } // ───── people: owner, roles, bans (AdminRoomPeopleController) ───── function who(w) { if (!w || !w.accountId) return 'nobody'; return (w.username ? '@' + esc(w.username) : '#' + w.accountId) + (w.displayName && w.displayName !== w.username ? ' (' + esc(w.displayName) + ')' : '') + ' #' + w.accountId + ''; } function when(t) { return t ? new Date(t).toLocaleString() : ''; } var BAN_LENGTHS = [[0, 'Until unbanned'], [60, '1 hour'], [1440, '1 day'], [10080, '1 week'], [43200, '30 days']]; function peoplePost(roomId, what, fields) { var f = new FormData(); Object.keys(fields).forEach(function (k) { f.append(k, fields[k]); }); var msg = document.getElementById('rp-msg'); if (msg) { msg.style.color = 'var(--muted)'; msg.textContent = 'Working…'; } return fetch('/admin/api/room/' + roomId + '/' + what, { method: 'POST', body: f, credentials: 'same-origin' }) .then(function (r) { return r.json(); }) .then(function (d) { loadPeople(roomId, d.message, d.success); return d; }) .catch(function () { if (msg) { msg.style.color = 'var(--danger)'; msg.textContent = 'That failed.'; } }); } function loadPeople(roomId, note, ok) { var box = document.getElementById('re-people'); if (!box) return; fetch('/admin/api/room/' + encodeURIComponent(roomId) + '/people', { credentials: 'same-origin' }) .then(function (r) { return r.json(); }) .then(function (d) { if (!d.success) { box.innerHTML = '

' + esc(d.message || 'Could not load people.') + '

'; return; } var roleOpts = function (sel) { return d.grantable.map(function (g) { return ''; }).join(''); }; var rows = d.roles.length ? d.roles.map(function (r) { return '
' + '' + who(r.who) + (r.pending ? ' invited as ' + esc(r.pendingRole) + '' : '') + '' + '' + '
'; }).join('') : '

Nobody else has a role on this room.

'; var bans = d.bans.length ? d.bans.map(function (b) { return '
' + '' + who(b.who) + '
by ' + who(b.by) + ' · ' + when(b.start) + (b.end ? ' · until ' + when(b.end) : ' · until unbanned') + (b.reason ? ' · “' + esc(b.reason) + '”' : '') + '
' + '
'; }).join('') : '

Nobody is banned from this room.

'; var hist = d.history.length ? '
Earlier bans (' + d.history.length + ')' + d.history.map(function (h) { return '
' + who(h.who) + ' - banned by ' + who(h.by) + ' ' + when(h.start) + (h.reason ? ' “' + esc(h.reason) + '”' : '') + ' - ' + esc(h.lifted) + '' + (h.unbannedBy ? ' by ' + who(h.unbannedBy) : '') + '
'; }).join('') + '
' : ''; box.innerHTML = '

People

' + '

' + esc(note || '') + '

' + '
Owner: ' + who(d.owner) + '
' + (d.isDorm ? '

This is a dorm - it belongs to its player and can\'t be given away.

' : '
' + '' + '
') + '
Roles
' + rows + '
' + '' + '
' + '
Bans
' + bans + hist + '
' + '' + '' + '
'; var $ = function (id) { return document.getElementById(id); }; box.querySelectorAll('.rp-role').forEach(function (sel) { sel.addEventListener('change', function () { var label = sel.options[sel.selectedIndex].text; if (sel.value === '0' && !confirm('Remove their role?')) { loadPeople(roomId); return; } peoplePost(roomId, 'role', { player: sel.dataset.id, role: sel.value }); }); }); box.querySelectorAll('.rp-ban').forEach(function (b) { b.addEventListener('click', function () { $('rp-banwho').value = b.dataset.id; $('rp-banwho').scrollIntoView({ block: 'center' }); $('rp-banwhy').focus(); }); }); box.querySelectorAll('.rp-unban').forEach(function (b) { b.addEventListener('click', function () { peoplePost(roomId, 'unban', { player: b.dataset.id }); }); }); var t = $('rp-transfer'); if (t) t.addEventListener('click', function () { var to = $('rp-owner').value.trim(); if (!to) { $('rp-owner').focus(); return; } if (!confirm('Give ^' + d.name + ' to ' + to + '? They become its owner.')) return; peoplePost(roomId, 'owner', { player: to, keepOld: $('rp-keep').checked ? 'true' : 'false' }); }); $('rp-roleadd').addEventListener('click', function () { var to = $('rp-rolewho').value.trim(); if (!to) { $('rp-rolewho').focus(); return; } peoplePost(roomId, 'role', { player: to, role: $('rp-rolepick').value }); }); $('rp-banadd').addEventListener('click', function () { var to = $('rp-banwho').value.trim(); if (!to) { $('rp-banwho').focus(); return; } if (!confirm('Ban ' + to + ' from ^' + d.name + '? If they are in it now they are sent out.')) return; peoplePost(roomId, 'ban', { player: to, minutes: $('rp-banlen').value, reason: $('rp-banwhy').value }); }); }) .catch(function () { box.innerHTML = '

Could not load people.

'; }); } window.openRoomEditor = openRoomEditor; })();