mirror of
https://github.com/myawired/2025-RecRoom-Server-E12354.git
synced 2026-07-17 16:11:15 +10:00
Initial upload
This commit is contained in:
@@ -0,0 +1,507 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Rec Room — Admin Panel</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0f1724; --panel:#0b1220; --muted:#9aa4b2; --accent:#2dd4bf; --danger:#ff6b6b;
|
||||
--glass: rgba(255,255,255,0.02);
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;font-family:Inter,system-ui,Segoe UI,Roboto,"Helvetica Neue",Arial;background:linear-gradient(180deg,var(--bg),#071021);color:#e6eef6}
|
||||
.wrap{max-width:1200px;margin:28px auto;padding:18px}
|
||||
header{display:flex;gap:16px;align-items:center;justify-content:space-between;padding:12px 18px;background:var(--glass);border-radius:12px;border:1px solid rgba(255,255,255,0.04)}
|
||||
.brand{display:flex;gap:12px;align-items:center}
|
||||
.logo{width:44px;height:44px;border-radius:10px;background:linear-gradient(135deg,var(--accent),#60a5fa);display:flex;align-items:center;justify-content:center;font-weight:700;color:#052b2a}
|
||||
.title{font-size:18px;font-weight:700}
|
||||
.controls{display:flex;gap:8px;align-items:center}
|
||||
button{background:transparent;border:1px solid rgba(255,255,255,0.06);padding:8px 10px;border-radius:8px;color:inherit;cursor:pointer}
|
||||
.grid{display:grid;grid-template-columns:280px 1fr;gap:16px;margin-top:18px}
|
||||
aside{background:var(--panel);padding:12px;border-radius:12px;border:1px solid rgba(255,255,255,0.04)}
|
||||
main{background:var(--panel);padding:14px;border-radius:12px;border:1px solid rgba(255,255,255,0.04)}
|
||||
nav ul{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:8px}
|
||||
nav li{display:flex;align-items:center;gap:8px}
|
||||
.menu-item{padding:10px;border-radius:8px;cursor:pointer}
|
||||
.menu-item.active{background:linear-gradient(90deg,#042027 0%, rgba(255,255,255,0.02) 100%);border:1px solid rgba(255,255,255,0.02)}
|
||||
.panel-block{background:rgba(255,255,255,0.02);padding:10px;border-radius:8px;margin-bottom:10px}
|
||||
.list{display:flex;flex-direction:column;gap:8px;max-height:300px;overflow:auto;padding-right:6px}
|
||||
.row{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px;border-radius:8px}
|
||||
.badge{font-size:12px;padding:6px 8px;border-radius:999px;background:rgba(255,255,255,0.03)}
|
||||
.muted{color:var(--muted);font-size:13px}
|
||||
.table{width:100%;border-collapse:collapse}
|
||||
.table th,.table td{padding:8px;text-align:left;border-bottom:1px dashed rgba(255,255,255,0.02);font-size:13px}
|
||||
input,select,textarea{padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:transparent;color:inherit;width:100%}
|
||||
.row-actions{display:flex;gap:6px}
|
||||
.danger{background:linear-gradient(90deg,var(--danger), #ff8a8a);color:#fff;border:none}
|
||||
.accent{background:linear-gradient(90deg,var(--accent), #60a5fa);color:#012; border:none}
|
||||
footer{margin-top:14px;color:var(--muted);font-size:13px}
|
||||
.logs{background:#031018;padding:10px;border-radius:8px;max-height:220px;overflow:auto;font-family:monospace;font-size:12px}
|
||||
.stat-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
|
||||
.stat{background:rgba(255,255,255,0.02);padding:12px;border-radius:10px;text-align:center}
|
||||
.player-online{color:#9ff0d9;font-weight:700}
|
||||
.login-box{max-width:360px;margin:28px auto;background:var(--panel);padding:18px;border-radius:12px}
|
||||
.hidden{display:none}
|
||||
@media(max-width:900px){.grid{grid-template-columns:1fr;}.stat-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<header>
|
||||
<div class="brand">
|
||||
<div class="logo">RR</div>
|
||||
<div>
|
||||
<div class="title">Rec Room — Admin</div>
|
||||
<div class="muted">Moderation & server controls</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div id="server-status" class="muted">Disconnected</div>
|
||||
<button id="btn-reconnect">Reconnect</button>
|
||||
<button id="btn-logout" class="hidden">Logout</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Login -->
|
||||
<div id="login" class="login-box">
|
||||
<h3>Admin login</h3>
|
||||
<p class="muted">Use an admin API token or username/password (demo only).</p>
|
||||
<div style="display:grid;gap:8px">
|
||||
<input id="login-username" placeholder="username (admin)" />
|
||||
<input id="login-password" placeholder="password (demo)" type="password" />
|
||||
<input id="login-token" placeholder="OR paste API token" />
|
||||
<div style="display:flex;gap:8px">
|
||||
<button id="btn-login" class="accent">Login</button>
|
||||
<button id="btn-demo" title="Quick demo token">Demo token</button>
|
||||
</div>
|
||||
<div id="login-msg" class="muted"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main dashboard -->
|
||||
<div id="dashboard" class="hidden">
|
||||
<div class="grid">
|
||||
<aside>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><div class="menu-item active" data-view="overview">Overview</div></li>
|
||||
<li><div class="menu-item" data-view="players">Players</div></li>
|
||||
<li><div class="menu-item" data-view="rooms">Rooms</div></li>
|
||||
<li><div class="menu-item" data-view="games">Games</div></li>
|
||||
<li><div class="menu-item" data-view="images">Images</div></li>
|
||||
<li><div class="menu-item" data-view="logs">Logs</div></li>
|
||||
<li><div class="menu-item" data-view="settings">Settings</div></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div style="margin-top:12px">
|
||||
<div class="panel-block">
|
||||
<div class="muted">Live</div>
|
||||
<div style="display:flex;gap:8px;align-items:center;margin-top:8px">
|
||||
<div><strong id="live-players">0</strong><div class="muted">players</div></div>
|
||||
<div style="margin-left:auto"><button id="btn-refresh">Refresh</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-block">
|
||||
<div class="muted">Server control</div>
|
||||
<div style="display:flex;gap:8px;margin-top:8px">
|
||||
<button id="btn-restart">Restart</button>
|
||||
<button id="btn-save">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-block">
|
||||
<div class="muted">Utilities</div>
|
||||
<div style="display:flex;gap:8px;margin-top:8px">
|
||||
<button id="btn-broadcast">Broadcast</button>
|
||||
<button id="btn-mass-kick">Kick All</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
<!-- dynamic views -->
|
||||
<div id="view-overview" class="view">
|
||||
<h2>Overview</h2>
|
||||
<div class="stat-grid" style="margin-top:12px">
|
||||
<div class="stat"><div class="muted">Players online</div><div id="stat-players" class="player-online">0</div></div>
|
||||
<div class="stat"><div class="muted">Active rooms</div><div id="stat-rooms">0</div></div>
|
||||
<div class="stat"><div class="muted">Games</div><div id="stat-games">0</div></div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top:16px">Live Players</h3>
|
||||
<div class="list" id="player-list" style="margin-top:8px"></div>
|
||||
|
||||
<h3 style="margin-top:16px">Recent events</h3>
|
||||
<div class="logs" id="live-logs"></div>
|
||||
</div>
|
||||
|
||||
<div id="view-players" class="view hidden">
|
||||
<h2>Players</h2>
|
||||
<table class="table" id="players-table" style="margin-top:12px">
|
||||
<thead><tr><th>id</th><th>name</th><th>online</th><th>room</th><th>actions</th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="view-rooms" class="view hidden">
|
||||
<h2>Rooms</h2>
|
||||
<div style="display:flex;gap:12px;margin-top:12px">
|
||||
<div style="flex:1">
|
||||
<table class="table" id="rooms-table"><thead><tr><th>id</th><th>title</th><th>players</th><th>actions</th></tr></thead><tbody></tbody></table>
|
||||
</div>
|
||||
<div style="width:320px">
|
||||
<div class="muted">Room actions</div>
|
||||
<div style="margin-top:8px">
|
||||
<input id="room-title" placeholder="Room title" />
|
||||
<input id="room-id" placeholder="Room ID (optional)" />
|
||||
<div style="display:flex;gap:8px;margin-top:8px">
|
||||
<button id="btn-create-room" class="accent">Create</button>
|
||||
<button id="btn-update-room">Update</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="view-games" class="view hidden">
|
||||
<h2>Games</h2>
|
||||
<table class="table" id="games-table"><thead><tr><th>id</th><th>title</th><th>created</th><th>actions</th></tr></thead><tbody></tbody></table>
|
||||
</div>
|
||||
|
||||
<div id="view-images" class="view hidden">
|
||||
<h2>Images</h2>
|
||||
<div class="muted">Simple image browser (placeholder)</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:10px" id="image-grid"></div>
|
||||
</div>
|
||||
|
||||
<div id="view-logs" class="view hidden">
|
||||
<h2>Server logs</h2>
|
||||
<div class="logs" id="server-logs"></div>
|
||||
</div>
|
||||
|
||||
<div id="view-settings" class="view hidden">
|
||||
<h2>Settings</h2>
|
||||
<label>API Base URL</label>
|
||||
<input id="setting-api" placeholder="http://localhost:3000" />
|
||||
<label style="margin-top:8px">WebSocket URL</label>
|
||||
<input id="setting-ws" placeholder="ws://localhost:3000" />
|
||||
<div style="display:flex;gap:8px;margin-top:8px">
|
||||
<button id="btn-save-settings">Save</button>
|
||||
<button id="btn-reset-settings">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer>Built for your custom Rec Room server • Minimal demo code • <span id="version">v1.0</span></footer>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* =========================
|
||||
CONFIG - edit if needed
|
||||
========================= */
|
||||
let API_BASE = localStorage.getItem('api_base') || 'http://localhost:3000';
|
||||
let WS_URL = localStorage.getItem('ws_url') || 'ws://localhost:3000';
|
||||
const state = { token: null, ws: null };
|
||||
|
||||
/* -------------------------
|
||||
Helpers
|
||||
------------------------- */
|
||||
function el(q,ctx=document){return ctx.querySelector(q)}
|
||||
function els(q,ctx=document){return Array.from(ctx.querySelectorAll(q))}
|
||||
function setText(q,t){const e=el(q); if(e) e.textContent=t}
|
||||
function logEvent(s){const container=el('#live-logs'); if(!container) return; const row=document.createElement('div'); row.textContent = '['+new Date().toLocaleTimeString()+'] '+s; container.prepend(row)}
|
||||
|
||||
/* -------------------------
|
||||
UI wiring
|
||||
------------------------- */
|
||||
(function initUI(){
|
||||
// menu switching
|
||||
els('.menu-item').forEach(mi=>{
|
||||
mi.addEventListener('click',()=>{
|
||||
els('.menu-item').forEach(x=>x.classList.remove('active'));
|
||||
mi.classList.add('active');
|
||||
const view = 'view-' + mi.dataset.view;
|
||||
els('.view').forEach(v=>v.classList.add('hidden'));
|
||||
el('#'+view).classList.remove('hidden');
|
||||
});
|
||||
});
|
||||
|
||||
// login
|
||||
el('#btn-login').addEventListener('click', async ()=>{
|
||||
const tokenInput = el('#login-token').value.trim();
|
||||
if(tokenInput){ // paste token
|
||||
state.token = tokenInput;
|
||||
afterLogin();
|
||||
return;
|
||||
}
|
||||
const username = el('#login-username').value.trim() || 'admin';
|
||||
const password = el('#login-password').value.trim() || 'demo';
|
||||
try{
|
||||
const res = await fetch(API_BASE + '/api/login', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({username,password})
|
||||
});
|
||||
if(!res.ok) throw new Error('login failed');
|
||||
const data = await res.json();
|
||||
state.token = data.token;
|
||||
afterLogin();
|
||||
}catch(err){
|
||||
el('#login-msg').textContent = 'Login failed: ' + err.message;
|
||||
}
|
||||
});
|
||||
|
||||
el('#btn-demo').addEventListener('click', ()=>{ el('#login-token').value = 'DEMO-TOKEN' });
|
||||
|
||||
el('#btn-logout').addEventListener('click', ()=>{
|
||||
state.token = null;
|
||||
el('#dashboard').classList.add('hidden');
|
||||
el('#login').classList.remove('hidden');
|
||||
el('#btn-logout').classList.add('hidden');
|
||||
if(state.ws) state.ws.close();
|
||||
});
|
||||
|
||||
// refresh button
|
||||
el('#btn-refresh').addEventListener('click', ()=>fetchOverview());
|
||||
el('#btn-restart').addEventListener('click', ()=>apiPost('/api/admin/restart',{}, true));
|
||||
el('#btn-save').addEventListener('click', ()=>apiPost('/api/admin/save',{}, true));
|
||||
el('#btn-broadcast').addEventListener('click', ()=> {
|
||||
const msg = prompt('Broadcast message to all players:');
|
||||
if(msg) apiPost('/api/admin/broadcast',{message:msg}, true);
|
||||
});
|
||||
el('#btn-mass-kick').addEventListener('click', ()=> {
|
||||
if(confirm('Kick all online players?')) apiPost('/api/admin/kick_all',{}, true);
|
||||
});
|
||||
|
||||
// room create/update
|
||||
el('#btn-create-room').addEventListener('click', async ()=>{
|
||||
const title = el('#room-title').value.trim(); if(!title) return alert('title required');
|
||||
await apiPost('/api/rooms',{title});
|
||||
fetchRooms();
|
||||
});
|
||||
el('#btn-update-room').addEventListener('click', async ()=>{
|
||||
const id = el('#room-id').value.trim(); if(!id) return alert('room id required');
|
||||
const title = el('#room-title').value.trim(); if(!title) return alert('title required');
|
||||
await apiPut('/api/rooms/'+id,{title});
|
||||
fetchRooms();
|
||||
});
|
||||
|
||||
// settings
|
||||
el('#setting-api').value = API_BASE;
|
||||
el('#setting-ws').value = WS_URL;
|
||||
el('#btn-save-settings').addEventListener('click', ()=>{
|
||||
API_BASE = el('#setting-api').value || API_BASE;
|
||||
WS_URL = el('#setting-ws').value || WS_URL;
|
||||
localStorage.setItem('api_base', API_BASE);
|
||||
localStorage.setItem('ws_url', WS_URL);
|
||||
alert('Saved');
|
||||
});
|
||||
el('#btn-reset-settings').addEventListener('click', ()=>{
|
||||
localStorage.removeItem('api_base'); localStorage.removeItem('ws_url');
|
||||
location.reload();
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
/* -------------------------
|
||||
After login
|
||||
------------------------- */
|
||||
function afterLogin(){
|
||||
el('#login').classList.add('hidden');
|
||||
el('#dashboard').classList.remove('hidden');
|
||||
el('#btn-logout').classList.remove('hidden');
|
||||
setText('#login-msg','');
|
||||
setText('#server-status','Connecting...');
|
||||
connectWS();
|
||||
fetchOverview();
|
||||
fetchPlayers();
|
||||
fetchRooms();
|
||||
fetchGames();
|
||||
fetchServerLogs();
|
||||
}
|
||||
|
||||
/* -------------------------
|
||||
API helpers
|
||||
------------------------- */
|
||||
function authHeaders(){
|
||||
const h = {'Content-Type':'application/json'};
|
||||
if(state.token) h['Authorization'] = 'Bearer ' + state.token;
|
||||
return h;
|
||||
}
|
||||
async function apiGet(path){ const res = await fetch(API_BASE + path, {headers:authHeaders()}); if(!res.ok) throw new Error(await res.text()); return res.json(); }
|
||||
async function apiPost(path, body={}, showOk=false){ const res = await fetch(API_BASE + path, {method:'POST',headers:authHeaders(),body:JSON.stringify(body)}); if(!res.ok) { alert('Error: '+await res.text()); return null } if(showOk) alert('OK'); return res.json(); }
|
||||
async function apiPut(path, body={}){ const res = await fetch(API_BASE + path, {method:'PUT',headers:authHeaders(),body:JSON.stringify(body)}); if(!res.ok) { alert('Error: '+await res.text()); return null } return res.json(); }
|
||||
async function apiDelete(path){ const res = await fetch(API_BASE + path, {method:'DELETE',headers:authHeaders()}); if(!res.ok) { alert('Error: '+await res.text()); return null } return res.json(); }
|
||||
|
||||
/* -------------------------
|
||||
Fetchers / renderers
|
||||
------------------------- */
|
||||
async function fetchOverview(){
|
||||
try{
|
||||
const data = await apiGet('/api/overview');
|
||||
setText('#stat-players', data.players_online);
|
||||
setText('#stat-rooms', data.rooms);
|
||||
setText('#stat-games', data.games);
|
||||
el('#player-list').innerHTML = '';
|
||||
data.players.slice(0,50).forEach(p=>{
|
||||
const div = document.createElement('div'); div.className='row';
|
||||
div.innerHTML = `<div style="display:flex;gap:12px;align-items:center"><div style="width:36px;height:36px;border-radius:8px;background:rgba(255,255,255,0.02);display:flex;align-items:center;justify-content:center">${p.name[0]||'?'}</div>
|
||||
<div><div style="font-weight:600">${p.name}</div><div class="muted" style="font-size:12px">id ${p.id}</div></div></div>
|
||||
<div><div class="badge">${p.room||'—'}</div></div>`;
|
||||
el('#player-list').appendChild(div);
|
||||
});
|
||||
}catch(err){
|
||||
console.error(err); setText('#server-status','Error'); logEvent('Overview fetch failed: '+err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPlayers(){
|
||||
try{
|
||||
const data = await apiGet('/api/players');
|
||||
const tbody = el('#players-table tbody'); tbody.innerHTML='';
|
||||
data.forEach(p=>{
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${p.id}</td><td>${p.name}</td><td>${p.online? 'Yes':'No'}</td><td>${p.room||''}</td>
|
||||
<td class="row-actions">
|
||||
<button onclick="inspectPlayer('${p.id}')">Inspect</button>
|
||||
<button onclick="kickPlayer('${p.id}')" class="danger">Kick</button>
|
||||
</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
setText('#live-players', data.filter(x=>x.online).length);
|
||||
}catch(err){ console.error(err) }
|
||||
}
|
||||
|
||||
async function fetchRooms(){
|
||||
try{
|
||||
const data = await apiGet('/api/rooms');
|
||||
const tbody = el('#rooms-table tbody'); tbody.innerHTML='';
|
||||
data.forEach(r=>{
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${r.id}</td><td>${r.title}</td><td>${r.players?.length||0}</td>
|
||||
<td class="row-actions">
|
||||
<button onclick="openRoom('${r.id}')">Open</button>
|
||||
<button onclick="deleteRoom('${r.id}')" class="danger">Delete</button>
|
||||
</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
setText('#stat-rooms', data.length);
|
||||
}catch(err){ console.error(err) }
|
||||
}
|
||||
|
||||
async function fetchGames(){
|
||||
try{
|
||||
const data = await apiGet('/api/games');
|
||||
const tbody = el('#games-table tbody'); tbody.innerHTML='';
|
||||
data.forEach(g=>{
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${g.id}</td><td>${g.title}</td><td>${new Date(g.created).toLocaleString()}</td>
|
||||
<td class="row-actions"><button onclick="deleteGame('${g.id}')" class="danger">Delete</button></td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
setText('#stat-games', data.length);
|
||||
}catch(err){ console.error(err) }
|
||||
}
|
||||
|
||||
async function fetchServerLogs(){
|
||||
try{
|
||||
const data = await apiGet('/api/logs');
|
||||
el('#server-logs').textContent = data.join('\n');
|
||||
}catch(err){ console.error(err) }
|
||||
}
|
||||
|
||||
/* -------------------------
|
||||
Actions
|
||||
------------------------- */
|
||||
window.inspectPlayer = async function(id){
|
||||
try{
|
||||
const p = await apiGet('/api/players/'+id);
|
||||
alert(JSON.stringify(p, null, 2));
|
||||
}catch(err){}
|
||||
}
|
||||
window.kickPlayer = async function(id){
|
||||
if(!confirm('Kick player '+id+'?')) return;
|
||||
await apiPost('/api/players/'+id+'/kick',{}, true);
|
||||
fetchPlayers();
|
||||
}
|
||||
window.openRoom = async function(id){
|
||||
// open room in a control view (placeholder)
|
||||
window.open(API_BASE + '/room/' + id, '_blank');
|
||||
}
|
||||
window.deleteRoom = async function(id){
|
||||
if(!confirm('Delete room '+id+'?')) return;
|
||||
await apiDelete('/api/rooms/'+id);
|
||||
fetchRooms();
|
||||
}
|
||||
window.deleteGame = async function(id){
|
||||
if(!confirm('Delete game '+id+'?')) return;
|
||||
await apiDelete('/api/games/'+id);
|
||||
fetchGames();
|
||||
}
|
||||
|
||||
/* -------------------------
|
||||
WebSocket realtime
|
||||
------------------------- */
|
||||
function connectWS(){
|
||||
if(state.ws) { try{ state.ws.close(); }catch(e){} }
|
||||
const wsUrl = WS_URL.replace(/^http/, 'ws');
|
||||
state.ws = new WebSocket(wsUrl + '/ws?token=' + encodeURIComponent(state.token || ''));
|
||||
state.ws.addEventListener('open', ()=> {
|
||||
setText('#server-status','Connected');
|
||||
logEvent('Connected to server');
|
||||
fetchPlayers(); fetchRooms();
|
||||
});
|
||||
state.ws.addEventListener('message', (ev)=>{
|
||||
try{
|
||||
const msg = JSON.parse(ev.data);
|
||||
handleWsMsg(msg);
|
||||
}catch(e){ console.log('WS msg parse error',e) }
|
||||
});
|
||||
state.ws.addEventListener('close', ()=>{ setText('#server-status','Disconnected'); logEvent('WS closed'); });
|
||||
state.ws.addEventListener('error', (e)=>{ setText('#server-status','Error'); console.error('ws error',e) });
|
||||
}
|
||||
|
||||
function handleWsMsg(msg){
|
||||
// expected messages: {type:'player_join', payload:{id,name,room}}, ... etc
|
||||
switch(msg.type){
|
||||
case 'player_join':
|
||||
logEvent(`JOIN ${msg.payload.name} (${msg.payload.id}) -> ${msg.payload.room}`);
|
||||
fetchPlayers();
|
||||
break;
|
||||
case 'player_leave':
|
||||
logEvent(`LEFT ${msg.payload.name} (${msg.payload.id})`);
|
||||
fetchPlayers();
|
||||
break;
|
||||
case 'room_created':
|
||||
logEvent(`Room created: ${msg.payload.title}`);
|
||||
fetchRooms();
|
||||
break;
|
||||
case 'broadcast':
|
||||
logEvent(`BROADCAST: ${msg.payload.message}`);
|
||||
break;
|
||||
default:
|
||||
console.log('ws unknown',msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* quick status ping */
|
||||
document.getElementById('btn-reconnect').addEventListener('click', ()=>{connectWS()});
|
||||
|
||||
/* first-run helpers */
|
||||
setInterval(()=>{ if(state.token) fetchOverview(); }, 15000);
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,139 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Rec Rewild Admin Login</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: url('/img/RRWD_nologo.jpg') no-repeat center center fixed;
|
||||
background-size: cover;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: rgb(55, 99, 12);
|
||||
padding: 40px;
|
||||
border-radius: 6px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
box-shadow: 0 4px 6px rgb(12, 99, 76);
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid rgb(35, 97, 16);
|
||||
background: rgb(35, 97, 16);
|
||||
color: white;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: rgb(49, 146, 19);
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: #0523107e;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
background: rgb(35, 97, 16);
|
||||
}
|
||||
|
||||
.divider {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.discord-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: #5865F2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.discord-btn:hover {
|
||||
background: #4752c4;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
color: #ffcccc;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<h2>Rec Rewild Admin Login</h2>
|
||||
|
||||
<form action="/api/web/admin/login" method="post">
|
||||
<div class="input-group">
|
||||
<label>Username</label>
|
||||
<input type="text" name="username" required>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>Password</label>
|
||||
<input type="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<button class="login-btn" type="submit">Login</button>
|
||||
</form>
|
||||
|
||||
<div class="divider">OR</div>
|
||||
|
||||
<form action="/api/auth/discord" method="get">
|
||||
<button class="discord-btn" type="submit">
|
||||
<i class="fab fa-discord"></i>
|
||||
Login with Discord
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="error-message" id="error-message"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,116 @@
|
||||
<!DOCTYPE html>
|
||||
<head>
|
||||
<title >
|
||||
rec rewild discord linker
|
||||
</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: url('/img/RRWD_nologo.jpg') no-repeat center center fixed;
|
||||
background-size: cover;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: rgb(24, 125, 184);
|
||||
padding: 40px;
|
||||
border-radius: 1px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
box-shadow: 0 4px 6px rgb(19, 95, 139);
|
||||
backdrop-filter: blur(1px);
|
||||
}
|
||||
|
||||
.login-container h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid rgb(24, 125, 184);
|
||||
border-radius: 5px;
|
||||
background: rgb(24, 125, 184);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.input-group input:focus {
|
||||
outline: none;
|
||||
border-color: rgb(24, 125, 184);
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: #0523107e;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
background-color: rgb(17, 87, 128);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.g-recaptcha {
|
||||
margin: 15px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<h2>Rec_rewild Discord Link Account</h2>
|
||||
<form action="/api/web/player/link" method="post" autocomplete="on">
|
||||
<div class="input-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="login-btn">Link</button>
|
||||
</form>
|
||||
<div class="error-message" id="error-message"></div>
|
||||
</div>
|
||||
</body>
|
||||
+447
@@ -0,0 +1,447 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Rec Rewild Live - Custom Rec Room 2023 Server with All Items</title>
|
||||
<meta name="description"
|
||||
content="Rec Rewild (aka Recrewild or Rec_rewild) is a custom Rec Room Live server for 2023 with all items unlocked. Play Rec Room your way!" />
|
||||
<meta name="keywords" content="Rec Rewild, Rec Room private server, Recrewild, Rec Room 2023, custom rec room" />
|
||||
<link rel="canonical" href="https://recrewild.oldrec.net" />
|
||||
<meta property="og:title" content="Rec Rewild - Custom Rec Room 2023 Server" />
|
||||
<meta property="og:description"
|
||||
content="Rec Rewild is a custom Rec Room Live server for 2023 with all items unlocked. Join and explore today!" />
|
||||
<meta property="og:url" content="https://recrewild.oldrec.net" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content="https://recrewild.oldrec.net/img/RRWD.jpg" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="Rec Rewild - Custom Rec Room Server 2023" />
|
||||
<meta name="twitter:description"
|
||||
content="Rec Rewild (aka Recrewild) is a custom Rec Room Live server for 2023 with all items unlocked." />
|
||||
<meta name="twitter:image" content="https://recrewild.oldrec.net/img/RRWD.jpg" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta name="googlebot" content="index, follow" />
|
||||
<meta name="theme-color" content="#00960c" />
|
||||
<meta name="google-site-verification" content="tuopCIjC9M4kQvPc8ugZ5C3-9R8hQ7SMdlDZre7rr8U" />
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #0f0f0f;
|
||||
--text-color: #e0ffe0;
|
||||
--accent-color: #00ff88;
|
||||
--glow: 0 0 6px #00ff8855;
|
||||
--glow-strong: 0 0 4px #00ff8855;
|
||||
}
|
||||
|
||||
body.blue-mode {
|
||||
--bg-color: #0a0e2a;
|
||||
--text-color: #e0f0ff;
|
||||
--accent-color: #3399ff;
|
||||
--glow: 0 0 6px #3399ff55;
|
||||
--glow-strong: 0 0 4px #3399ff88;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', 'Inter', sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
animation: bodyFade 0.8s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes bodyFade {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
background: rgba(16, 28, 16, 0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
padding: 1rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: var(--glow);
|
||||
}
|
||||
|
||||
body.blue-mode header {
|
||||
background: rgba(18, 30, 58, 0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: var(--accent-color);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
text-shadow: var(--glow);
|
||||
transition: transform 0.2s, opacity 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 3rem auto;
|
||||
padding: 0 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.8s ease forwards;
|
||||
}
|
||||
|
||||
.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.banner {
|
||||
width: 100%;
|
||||
max-width: 150px;
|
||||
height: auto;
|
||||
margin-bottom: 2rem;
|
||||
filter: drop-shadow(0 0 5px var(--accent-color));
|
||||
animation: pulseGlow 3s ease-in-out infinite;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.banner:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--accent-color);
|
||||
text-shadow: var(--glow);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--accent-color);
|
||||
text-shadow: var(--glow-strong);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulseGlow {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
filter: drop-shadow(0 0 5px var(--accent-color));
|
||||
}
|
||||
|
||||
50% {
|
||||
filter: drop-shadow(0 0 12px var(--accent-color));
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-theme {
|
||||
background: none;
|
||||
border: 1px solid var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
text-shadow: var(--glow);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.toggle-theme:hover {
|
||||
background-color: var(--accent-color);
|
||||
color: var(--bg-color);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.glitch {
|
||||
position: relative;
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
color: var(--accent-color);
|
||||
text-shadow: 0 0 2px var(--accent-color);
|
||||
}
|
||||
|
||||
.glitch::before,
|
||||
.glitch::after {
|
||||
content: attr(data-text);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.glitch::before {
|
||||
animation: glitchTop 1s infinite linear alternate-reverse;
|
||||
}
|
||||
|
||||
.glitch::after {
|
||||
animation: glitchBottom 1s infinite linear alternate-reverse;
|
||||
}
|
||||
|
||||
@keyframes glitchTop {
|
||||
0% {
|
||||
clip: rect(0, 9999px, 0, 0);
|
||||
}
|
||||
|
||||
5% {
|
||||
clip: rect(0, 9999px, 10px, 0);
|
||||
left: 2px;
|
||||
}
|
||||
|
||||
100% {
|
||||
clip: rect(0, 9999px, 0, 0);
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glitchBottom {
|
||||
0% {
|
||||
clip: rect(0, 9999px, 0, 0);
|
||||
}
|
||||
|
||||
5% {
|
||||
clip: rect(12px, 9999px, 22px, 0);
|
||||
left: -2px;
|
||||
}
|
||||
|
||||
100% {
|
||||
clip: rect(0, 9999px, 0, 0);
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.floating-bg {
|
||||
position: fixed;
|
||||
top: -20%;
|
||||
left: -20%;
|
||||
width: 140%;
|
||||
height: 140%;
|
||||
background: radial-gradient(circle at center, var(--accent-color) 0%, transparent 60%);
|
||||
opacity: 0.03;
|
||||
animation: floatBg 12s infinite alternate ease-in-out;
|
||||
filter: blur(80px);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes floatBg {
|
||||
from {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate(10%, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.flex-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
ul li {
|
||||
transition: all 0.3s ease;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
ul li:hover {
|
||||
text-shadow: 0 0 6px var(--accent-color);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 3rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
#scrollTopBtn {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
background: var(--accent-color);
|
||||
color: var(--bg-color);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 10px 15px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--glow);
|
||||
display: none;
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
#scrollTopBtn:hover {
|
||||
transform: translateY(-3px);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.glitch {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.branding {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#header-logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
filter: drop-shadow(0 0 5px var(--accent-color));
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
#header-logo:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--accent-color);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 0 12px var(--accent-color);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="floating-bg"></div>
|
||||
<header>
|
||||
<div class="branding">
|
||||
<img src="/img/DefaultImgGreen.png" alt="Rec Rewild Logo" id="header-logo">
|
||||
<div class="glitch" data-text="Rec_rewild">Rec Rewild</div>
|
||||
</div>
|
||||
<nav>
|
||||
<a onclick="GoTo('rewild_net')">Rewild Net</a>
|
||||
<a onclick="GoTo('admin/v4/login')">Admin Login</a>
|
||||
<a
|
||||
onclick="GoTo('https://discord.com/invite/sqUBwhcY')">Discord</a>
|
||||
<button class="toggle-theme" onclick="ToggleTheme()">Blue Theme</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container flex-wrapper">
|
||||
<section id="home" class="page active">
|
||||
<img id="logo-home" class="banner" src="/img/DefaultImgGreen.png" alt="Rec Rewild Logo">
|
||||
<h1>Welcome to Rec Rewild</h1>
|
||||
<p>
|
||||
<strong>Rec Rewild</strong> is a custom <strong>Rec Room 2023</strong> server built by
|
||||
<strong>e12354</strong>
|
||||
</p>
|
||||
|
||||
<div class="feature-grid">
|
||||
<div class="feature-card">✨ <strong>All Items Unlocked</strong><br>Access every outfit, food, and weapon
|
||||
skin.</div>
|
||||
<div class="feature-card">👥 <strong>Active Community</strong><br>Join live events, hangouts, and
|
||||
Discord tournaments.</div>
|
||||
<div class="feature-card">🔒 <strong>No Paywalls</strong><br>Everything is open, fair, and free for
|
||||
everyone.</div>
|
||||
<div class="feature-card">💾 <strong>Frequent Updates</strong><br>New features and content added
|
||||
regularly.</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>© <span id="year"></span> Rec Rewild. Not affiliated with Rec Room or Against Gravity.</footer>
|
||||
|
||||
<button id="scrollTopBtn" onclick="ScrollToTop()">▲ Top</button>
|
||||
|
||||
<script>
|
||||
function ShowPage(id) {
|
||||
document.querySelectorAll('.page').forEach(page => {
|
||||
page.classList.remove('active');
|
||||
page.style.opacity = 0;
|
||||
});
|
||||
const target = document.getElementById(id);
|
||||
target.classList.add('active');
|
||||
target.style.animation = 'none';
|
||||
void target.offsetWidth;
|
||||
target.style.animation = 'fadeIn 0.6s ease forwards';
|
||||
}
|
||||
|
||||
function GoTo(page) {
|
||||
window.location.href = page;
|
||||
}
|
||||
|
||||
function ToggleTheme() {
|
||||
const isBlue = document.body.classList.toggle("blue-mode");
|
||||
const newLogo = isBlue ? "/img/DefaultImg.png" : "/img/DefaultImgGreen.png";
|
||||
document.getElementById("logo-home").src = newLogo;
|
||||
document.getElementById("header-logo").src = newLogo;
|
||||
document.querySelector(".toggle-theme").textContent = isBlue ? "Green Theme" : "Blue Theme";
|
||||
localStorage.setItem("rewild-theme", isBlue ? "blue" : "green");
|
||||
}
|
||||
|
||||
function ScrollToTop() {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", () => {
|
||||
const btn = document.getElementById("scrollTopBtn");
|
||||
btn.style.display = window.scrollY > 200 ? "block" : "none";
|
||||
});
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
document.getElementById("year").textContent = new Date().getFullYear();
|
||||
const savedTheme = localStorage.getItem("rewild-theme");
|
||||
if (savedTheme === "blue") {
|
||||
document.body.classList.add("blue-mode");
|
||||
document.getElementById("logo-home").src = "/img/DefaultImg.png";
|
||||
document.getElementById("header-logo").src = "/img/DefaultImg.png";
|
||||
document.querySelector(".toggle-theme").textContent = "Green Theme";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,818 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Home - Rewild Net</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" />
|
||||
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
margin: 0;
|
||||
background: #04140c;
|
||||
color: #c9ffe5;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ===== SIDEBAR ===== */
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background: rgba(10, 60, 40, 0.95);
|
||||
height: 100vh;
|
||||
padding: 20px;
|
||||
transition: left 0.3s ease;
|
||||
box-shadow: 3px 0 15px rgba(0, 255, 170, 0.35);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sidebar ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.sidebar li {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.sidebar button,
|
||||
.sidebar a {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
color: #bfffe6;
|
||||
background-color: rgba(0, 255, 170, 0.12);
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s, color 0.3s;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sidebar button:hover,
|
||||
.sidebar a:hover,
|
||||
.sidebar button.active,
|
||||
.sidebar a.active {
|
||||
background-color: #1aff9c;
|
||||
color: #00331f;
|
||||
box-shadow: 0 0 12px #1aff9c;
|
||||
}
|
||||
|
||||
/* ===== YOU BUTTON ===== */
|
||||
#you-button {
|
||||
background-color: #1fa774;
|
||||
color: #00331f;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 0 12px #1aff9c;
|
||||
}
|
||||
|
||||
#you-button:hover {
|
||||
background-color: #1aff9c;
|
||||
box-shadow: 0 0 18px #1aff9c;
|
||||
}
|
||||
|
||||
#you-button img {
|
||||
border-radius: 10px;
|
||||
margin-right: 10px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* ===== MAIN CONTENT ===== */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
background: linear-gradient(135deg, #062b1b, #0b4a32);
|
||||
border-radius: 0 0 0 12px;
|
||||
box-shadow: inset 0 0 20px rgba(0, 255, 170, 0.2);
|
||||
}
|
||||
|
||||
/* ===== BANNER ===== */
|
||||
.banner {
|
||||
background: rgba(0, 255, 170, 0.2);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 0 15px rgba(0, 255, 170, 0.4);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ===== CARDS ===== */
|
||||
#home-feed .card {
|
||||
position: relative;
|
||||
border-radius: 15px;
|
||||
border: none;
|
||||
box-shadow: 0 8px 20px rgba(0, 255, 170, 0.35);
|
||||
transition: transform 0.25s ease, box-shadow 0.25s ease;
|
||||
background-color: #0f4a34;
|
||||
color: #eafff6;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#home-feed .card:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 15px 30px rgba(0, 255, 170, 0.7);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.player-overlay {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 10px;
|
||||
padding: 5px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #bfffe6;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* ===== PROFILE ===== */
|
||||
.profile-banner-container {
|
||||
background: #0b3b29;
|
||||
box-shadow: 0 4px 20px rgba(0, 255, 170, 0.3);
|
||||
}
|
||||
|
||||
.profile-pic-overlay {
|
||||
border: 4px solid #1aff9c;
|
||||
box-shadow: 0 0 25px #1aff9c;
|
||||
background: #04140c;
|
||||
}
|
||||
|
||||
.profile-username {
|
||||
color: #bfffe6;
|
||||
text-shadow: 0 0 5px #1aff9c, 0 0 10px #1aff9c, 0 0 15px #1aff9c;
|
||||
}
|
||||
|
||||
.profile-displayname {
|
||||
color: #9dffd6;
|
||||
}
|
||||
|
||||
.account-id {
|
||||
color: #6bffbe;
|
||||
}
|
||||
|
||||
/* ===== BIO ===== */
|
||||
.bio-section {
|
||||
background: rgba(0, 255, 170, 0.12);
|
||||
color: #c9ffe5;
|
||||
box-shadow: inset 0 0 14px rgba(0, 255, 170, 0.4);
|
||||
}
|
||||
|
||||
/* ===== POPUP ===== */
|
||||
#popup-overlay {
|
||||
background: rgba(0, 15, 8, 0.9);
|
||||
}
|
||||
|
||||
#popup-box {
|
||||
background: #0b3b29;
|
||||
box-shadow: 0 0 25px #1aff9c;
|
||||
color: #eafff6;
|
||||
}
|
||||
|
||||
#popup-box button {
|
||||
background: #1fa774;
|
||||
box-shadow: 0 0 15px #1aff9c;
|
||||
}
|
||||
|
||||
#popup-box button:hover {
|
||||
background-color: #1aff9c;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="layout">
|
||||
<nav class="sidebar" id="sidebar">
|
||||
<ul>
|
||||
<div id="you-button-container" class="d-flex align-items-center mb-4">
|
||||
<button id="you-button" class="btn btn-info" onclick="goToMyProfile()">
|
||||
<img id="profile-image" src="/img/DefaultImg.png" alt="Profile" />
|
||||
YOU
|
||||
</button>
|
||||
</div>
|
||||
<li><button class="tab active" data-tab="home">Home</button></li>
|
||||
<li><button class="tab" data-tab="friends">Friends</button></li>
|
||||
<li><button class="tab" data-tab="rooms">Rooms</button></li>
|
||||
<li><button class="tab" data-tab="chats">Chats</button></li>
|
||||
<li><button class="tab" data-tab="settings">Settings</button></li>
|
||||
<li><button onclick="window.location.href='/rewild_net/logout'" class="bottom_part">Logout</button></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="main-content">
|
||||
<div id="home" class="content active" style="transition: opacity 0.3s;">
|
||||
<h3>Check out what's happening right now in Rec Rewild</h3>
|
||||
<div id="home-feed" class="row"></div>
|
||||
</div>
|
||||
|
||||
<div id="friends" class="content" style="display: none; transition: opacity 0.3s;">
|
||||
</div>
|
||||
|
||||
<div id="rooms" class="content" style="display: none; transition: opacity 0.3s;">
|
||||
<h3>Rooms</h3>
|
||||
<input type="text" id="room-search" class="form-control mb-3" placeholder="Search rooms..."
|
||||
oninput="searchRooms()" />
|
||||
<div id="room-results" class="row" style="gap: 15px;"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="settings" class="content" style="display: none; transition: opacity 0.3s;">
|
||||
</div>
|
||||
|
||||
<div id="chats" class="content" style="display: none; transition: opacity 0.3s;">
|
||||
<h3>Chats</h3>
|
||||
|
||||
<div class="row" style="height: 100%;">
|
||||
|
||||
<!-- LEFT SIDE — CHAT LIST -->
|
||||
<div id="chat-list" style="
|
||||
width: 280px;
|
||||
height: 85vh;
|
||||
overflow-y: auto;
|
||||
background: rgba(92,52,138,0.3);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 0 12px #6b3cc4;
|
||||
padding: 10px;
|
||||
">
|
||||
<div class="text-center" style="opacity: .7;">Loading chats...</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT SIDE — MESSAGES -->
|
||||
<div style="flex: 1; margin-left: 15px;">
|
||||
<div id="chat-window" style="
|
||||
height: 70vh;
|
||||
overflow-y: auto;
|
||||
padding: 15px;
|
||||
background: rgba(92,52,138,0.25);
|
||||
border-radius: 12px;
|
||||
box-shadow: inset 0 0 12px #a67fff88;
|
||||
margin-bottom: 10px;
|
||||
">
|
||||
<div class="text-center" style="opacity: .6;">Select a chat to view messages</div>
|
||||
</div>
|
||||
|
||||
<!-- MESSAGE INPUT -->
|
||||
<div class="d-flex">
|
||||
<input id="chat-input" class="form-control" placeholder="Type a message..." style="
|
||||
background: rgba(255,255,255,0.1);
|
||||
border: 1px solid #6b3cc4;
|
||||
color: white;
|
||||
">
|
||||
<button class="btn btn-primary ml-2" onclick="sendChatMessage()">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="profile" class="content" style="display: none; transition: opacity 0.3s;">
|
||||
<button id="back-to-home" onclick="showHome()">← Back to Home</button>
|
||||
<div id="profile-content" class="mt-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="popup-overlay">
|
||||
<div id="popup-box">
|
||||
<div id="popup-message"></div>
|
||||
<button onclick="closePopup()">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentUserId = null;
|
||||
let chatSelected = null;
|
||||
let chatMessages = {};
|
||||
let searchTimeout = null;
|
||||
|
||||
// ------------------ FETCH HELPERS ------------------
|
||||
async function fetchAsync(url) {
|
||||
const res = await fetch(url);
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
async function fetchPlayersBulk(playerIds) {
|
||||
if (!playerIds.length) return {};
|
||||
const query = playerIds.map(id => `id=${encodeURIComponent(id)}`).join('&');
|
||||
try {
|
||||
const data = await fetchAsync(`/Accounts/account/bulk?${query}`);
|
||||
return Object.fromEntries(data.map(p => [p.accountId, p]));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchBio(playerId) {
|
||||
try {
|
||||
const bioData = await fetchAsync(`/Accounts/account/${playerId}/bio`);
|
||||
return bioData?.Bio || "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------ ACCOUNT ------------------
|
||||
async function AccountMe() {
|
||||
try {
|
||||
const data = await fetchAsync("/Accounts/account/me");
|
||||
if (!data) return;
|
||||
|
||||
currentUserId = data.accountId || data.id || null;
|
||||
const profileImageUrl = `/img/${data.profileImage}`;
|
||||
document.getElementById("profile-image").src = profileImageUrl;
|
||||
document.getElementById("you-button").innerHTML = `
|
||||
<img id="profile-image" src="${profileImageUrl}" alt="Profile" />
|
||||
YOU (${data.username})
|
||||
`;
|
||||
} catch { }
|
||||
}
|
||||
|
||||
// ------------------ HOME FEED ------------------
|
||||
async function loadHomeFeed() {
|
||||
try {
|
||||
const feedData = await fetchAsync("https://recrewild.oldrec.net/api/images/v3/feed/global");
|
||||
const container = document.getElementById("home-feed");
|
||||
container.innerHTML = "";
|
||||
|
||||
feedData.sort((a, b) => new Date(b.CreatedAt) - new Date(a.CreatedAt));
|
||||
|
||||
const playerIds = [...new Set(feedData.filter(i => i.PlayerId).map(i => i.PlayerId))];
|
||||
const players = await fetchPlayersBulk(playerIds);
|
||||
|
||||
feedData.forEach(image => {
|
||||
const col = document.createElement("div");
|
||||
col.className = "col-md-3 col-sm-6 mb-4";
|
||||
|
||||
const card = document.createElement("div");
|
||||
card.className = "card bg-dark text-light";
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.loading = "lazy";
|
||||
img.src = `https://recrewild.oldrec.net/img/${image.ImageName}?width=400`;
|
||||
img.className = "card-img-top";
|
||||
img.alt = "Feed Image";
|
||||
card.appendChild(img);
|
||||
|
||||
if (image.PlayerId && players[image.PlayerId]) {
|
||||
const player = players[image.PlayerId];
|
||||
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "player-overlay";
|
||||
|
||||
const pfp = document.createElement("img");
|
||||
pfp.loading = "lazy";
|
||||
pfp.src = `/img/${player.profileImage}?width=96&cropSquare=true`;
|
||||
pfp.alt = `${player.username} profile`;
|
||||
overlay.appendChild(pfp);
|
||||
|
||||
const usernameSpan = document.createElement("span");
|
||||
usernameSpan.textContent = player.username;
|
||||
overlay.appendChild(usernameSpan);
|
||||
|
||||
card.appendChild(overlay);
|
||||
|
||||
card.style.cursor = "pointer";
|
||||
card.onclick = () => loadProfilePage(image.PlayerId);
|
||||
}
|
||||
|
||||
col.appendChild(card);
|
||||
container.appendChild(col);
|
||||
});
|
||||
} catch { }
|
||||
}
|
||||
|
||||
// ------------------ PROFILE ------------------
|
||||
async function loadProfilePage(playerId) {
|
||||
if (!playerId) return popup("Invalid player ID.");
|
||||
|
||||
const players = await fetchPlayersBulk([playerId]);
|
||||
const player = players[playerId];
|
||||
if (!player) return popup("Player not found.");
|
||||
|
||||
const bio = await fetchBio(playerId);
|
||||
|
||||
document.querySelectorAll('.content').forEach(c => {
|
||||
c.classList.remove('active');
|
||||
c.style.display = 'none';
|
||||
c.style.opacity = 0;
|
||||
});
|
||||
|
||||
const profileDiv = document.getElementById("profile");
|
||||
profileDiv.classList.add('active');
|
||||
profileDiv.style.display = 'block';
|
||||
setTimeout(() => profileDiv.style.opacity = 1, 10);
|
||||
|
||||
const bannerUrl = player.bannerImage
|
||||
? `/img/${player.bannerImage}?width=1200&height=300&cropSquare=false`
|
||||
: `/img/DefaultRoomImage.png?width=1200&height=300&cropSquare=false`;
|
||||
|
||||
document.getElementById("profile-content").innerHTML = `
|
||||
<div class="profile-banner-container">
|
||||
<img class="profile-banner" src="${bannerUrl}" alt="${player.username}'s banner" />
|
||||
<img class="profile-pic-overlay" src="/img/${player.profileImage}?width=256&cropSquare=true" alt="${player.username}'s profile" loading="lazy" />
|
||||
</div>
|
||||
<h2 class="profile-username">
|
||||
${player.username} <span class="emoji">${player.displayEmoji || ''}</span>
|
||||
</h2>
|
||||
${player.displayName && player.displayName.toLowerCase() !== player.username.toLowerCase() ? `<div class="profile-displayname">${player.displayName}</div>` : ''}
|
||||
<div class="account-id">ID: ${player.accountId}</div>
|
||||
<div class="bio-section">${bio || "<i>This player hasn't written a bio yet!</i>"}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ------------------ FRIENDS ------------------
|
||||
async function loadFriends() {
|
||||
try {
|
||||
const data = await fetchAsync("/api/relationships/v2/get");
|
||||
const friends = data.filter(rel => rel.RelationshipType === 3);
|
||||
const ids = [...new Set(friends.map(f => f.PlayerID))];
|
||||
|
||||
const container = document.getElementById("friends");
|
||||
if (!ids.length) {
|
||||
container.innerHTML = `<div class="banner">You have no friends added yet.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const playerMap = await fetchPlayersBulk(ids);
|
||||
container.innerHTML = '<div class="row" id="friends-list" style="gap: 15px; padding: 10px;"></div>';
|
||||
const list = document.getElementById("friends-list");
|
||||
|
||||
ids.forEach(id => {
|
||||
const player = playerMap[id];
|
||||
if (!player) return;
|
||||
|
||||
const box = document.createElement("div");
|
||||
box.className = "d-flex align-items-center p-2";
|
||||
box.style.cssText = `
|
||||
background: rgba(92,52,138,0.3);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 0 10px #6b3cc4;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
`;
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.src = `/img/${player.profileImage}?width=96&cropSquare=true`;
|
||||
img.alt = player.username;
|
||||
img.loading = "lazy";
|
||||
img.style.cssText = `
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
object-fit: cover;
|
||||
margin-right: 12px;
|
||||
border: 2px solid #8047d6;
|
||||
cursor: pointer;
|
||||
`;
|
||||
img.onclick = () => loadProfilePage(id);
|
||||
|
||||
const name = document.createElement("span");
|
||||
name.textContent = player.username;
|
||||
name.style.cssText = `font-weight: 600; color: #e0d9ff; cursor: pointer;`;
|
||||
name.onclick = () => loadProfilePage(id);
|
||||
|
||||
box.appendChild(img);
|
||||
box.appendChild(name);
|
||||
list.appendChild(box);
|
||||
});
|
||||
} catch {
|
||||
document.getElementById("friends").innerHTML = `<div class="banner">Failed to load friends.</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------ CHAT ------------------
|
||||
async function loadChats() {
|
||||
const container = document.getElementById("chat-list");
|
||||
container.innerHTML = "<div class='text-center' style='opacity:.6;'>Loading chats...</div>";
|
||||
|
||||
let threads = [];
|
||||
try {
|
||||
threads = await fetchAsync("/chat/thread");
|
||||
} catch {
|
||||
container.innerHTML = "<div class='banner'>Failed to load chats</div>";
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(threads) || !threads.length) {
|
||||
container.innerHTML = "<div class='banner'>No chats found</div>";
|
||||
return;
|
||||
}
|
||||
|
||||
const allPlayerIds = new Set();
|
||||
threads.forEach(t => t.playerIds?.forEach(id => allPlayerIds.add(id)));
|
||||
const profileMap = await fetchPlayersBulk([...allPlayerIds]);
|
||||
|
||||
threads.sort((a, b) => (new Date(b.latestMessage?.timeSent || 0)) - (new Date(a.latestMessage?.timeSent || 0)));
|
||||
|
||||
container.innerHTML = "";
|
||||
threads.forEach(thread => {
|
||||
const latest = thread.latestMessage;
|
||||
let lastText = "No messages yet";
|
||||
if (latest?.contents) {
|
||||
try { lastText = ParseChatmsg(latest.contents); } catch { lastText = "[invalid message]"; }
|
||||
}
|
||||
|
||||
const otherId = thread.playerIds?.find(id => id !== currentUserId);
|
||||
const profile = profileMap[otherId] || {};
|
||||
|
||||
const row = document.createElement("div");
|
||||
row.className = "d-flex align-items-center mb-2";
|
||||
row.style.cssText = `
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
background: rgba(92,52,138,0.25);
|
||||
cursor: pointer;
|
||||
transition: background .2s;
|
||||
`;
|
||||
row.onclick = () => openChat(thread.chatThreadId);
|
||||
|
||||
row.innerHTML = `
|
||||
<img src="${profile.avatarUrl || `/img/${otherId}?size=64`}"
|
||||
style="width:40px;height:40px;border-radius:10px;margin-right:10px;">
|
||||
<div>
|
||||
<div style="font-weight:600;color:#e0d9ff">${profile.username || `User ${otherId}`}</div>
|
||||
<div style="font-size:.85rem;opacity:.7;">${escapeHtml(lastText)}</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------ CHAT HELPERS ------------------
|
||||
function escapeHtml(str) {
|
||||
return str.replace(/[&<>"']/g, c => ({
|
||||
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
|
||||
}[c]));
|
||||
}
|
||||
|
||||
function ParseChatmsg(data) {
|
||||
const obj = JSON.parse(data);
|
||||
let text = obj?.Data || "";
|
||||
if (obj?.Version === 2 && obj?.Type === 0) text = text.replace(/<=>/g, "");
|
||||
return text;
|
||||
}
|
||||
|
||||
async function openChat(chatId) {
|
||||
chatSelected = chatId;
|
||||
const win = document.getElementById("chat-window");
|
||||
win.innerHTML = "<div class='text-center' style='opacity:.6;'>Loading messages...</div>";
|
||||
|
||||
try {
|
||||
chatMessages[chatId] = await fetchAsync(`/chat/thread/${chatId}/message`);
|
||||
renderChatMessages();
|
||||
} catch {
|
||||
win.innerHTML = `<div class="banner">Failed to load messages.</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function renderChatMessages() {
|
||||
const win = document.getElementById("chat-window");
|
||||
const msgs = chatMessages[chatSelected] || [];
|
||||
win.innerHTML = "";
|
||||
|
||||
const playerIds = [...new Set(msgs.map(m => m.senderPlayerId))];
|
||||
const profiles = await fetchPlayersBulk(playerIds);
|
||||
|
||||
msgs.forEach(m => {
|
||||
const mine = m.senderPlayerId === currentUserId;
|
||||
const player = profiles[m.senderPlayerId] || {};
|
||||
const senderName = player.username ? "@" + player.username : "User " + m.senderPlayerId;
|
||||
const text = ParseChatmsg(m.contents);
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.style.cssText = `margin:10px 0; display:flex; flex-direction:column; align-items:${mine ? "flex-end" : "flex-start"}`;
|
||||
|
||||
const nameDiv = document.createElement("div");
|
||||
nameDiv.style.cssText = `font-size:0.75rem; opacity:.7; margin-bottom:3px; ${mine ? "text-align:right;" : ""}`;
|
||||
nameDiv.textContent = senderName;
|
||||
|
||||
const bubble = document.createElement("div");
|
||||
bubble.style.cssText = `
|
||||
background:${mine ? "#6b3cc4aa" : "rgba(255,255,255,0.12)"};
|
||||
color:white;
|
||||
padding:10px 14px;
|
||||
border-radius:12px;
|
||||
max-width:70%;
|
||||
box-shadow:0 0 10px rgba(120,60,180,0.5);
|
||||
`;
|
||||
bubble.textContent = text;
|
||||
|
||||
wrapper.appendChild(nameDiv);
|
||||
wrapper.appendChild(bubble);
|
||||
win.appendChild(wrapper);
|
||||
});
|
||||
|
||||
win.scrollTop = win.scrollHeight;
|
||||
}
|
||||
|
||||
async function sendChatMessage() {
|
||||
if (!chatSelected) return;
|
||||
const input = document.getElementById("chat-input");
|
||||
const text = input.value.trim();
|
||||
if (!text) return;
|
||||
|
||||
const json = JSON.stringify(
|
||||
{
|
||||
Type: 0,
|
||||
Version: 2,
|
||||
Data: "<=>" + text,
|
||||
})
|
||||
|
||||
|
||||
input.value = "";
|
||||
await fetch(`/chat/thread/${chatSelected}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: "messageContents=" + json
|
||||
});
|
||||
|
||||
chatMessages[chatSelected].push({
|
||||
text,
|
||||
senderPlayerId: currentUserId
|
||||
});
|
||||
renderChatMessages();
|
||||
}
|
||||
|
||||
// ------------------ TAB & POPUP ------------------
|
||||
function setup_html_show(data_tab) {
|
||||
document.querySelectorAll('.content').forEach(c => {
|
||||
c.classList.remove('active');
|
||||
c.style.display = 'none';
|
||||
c.style.opacity = 0;
|
||||
});
|
||||
|
||||
const tabContent = document.getElementById(data_tab);
|
||||
tabContent.classList.add('active');
|
||||
tabContent.style.display = 'block';
|
||||
setTimeout(() => tabContent.style.opacity = 1, 10);
|
||||
|
||||
if (!tabContent.dataset.loaded) {
|
||||
tabContent.dataset.loaded = "true";
|
||||
if (data_tab === "home") loadHomeFeed();
|
||||
else if (data_tab === "friends") loadFriends();
|
||||
else if (data_tab === "rooms") loadHotRooms();
|
||||
else if (data_tab === "chats") loadChats();
|
||||
}
|
||||
}
|
||||
|
||||
function setup_html() {
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab').forEach(b => b.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
setup_html_show(tab.getAttribute('data-tab'));
|
||||
});
|
||||
});
|
||||
setup_html_show("home");
|
||||
}
|
||||
|
||||
function popup(message) {
|
||||
const overlay = document.getElementById("popup-overlay");
|
||||
const messageBox = document.getElementById("popup-message");
|
||||
messageBox.textContent = message;
|
||||
overlay.classList.add("active");
|
||||
}
|
||||
|
||||
function closePopup() {
|
||||
document.getElementById("popup-overlay").classList.remove("active");
|
||||
}
|
||||
|
||||
function showHome() {
|
||||
setup_html_show("home");
|
||||
}
|
||||
|
||||
function goToMyProfile() {
|
||||
if (currentUserId) loadProfilePage(currentUserId);
|
||||
else popup("You are not logged in!");
|
||||
}
|
||||
|
||||
// ------------------ ROOMS ------------------
|
||||
async function loadHotRooms() {
|
||||
try {
|
||||
const data = await fetchAsync("/Room_server/rooms/hot?tag=rro");
|
||||
const rooms = data?.Results || [];
|
||||
const container = document.getElementById("room-results");
|
||||
container.innerHTML = "";
|
||||
if (!rooms.length) {
|
||||
container.innerHTML = `<div class="banner">No hot rooms found.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
rooms.forEach(room => {
|
||||
const col = document.createElement("div");
|
||||
col.className = "col-md-3 col-sm-6";
|
||||
|
||||
const card = document.createElement("div");
|
||||
card.className = "card";
|
||||
card.style.background = "#4a308a";
|
||||
card.style.color = "#eee";
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.src = `/img/${room.ImageName}?width=400`;
|
||||
img.className = "card-img-top";
|
||||
img.alt = room.Name;
|
||||
img.loading = "lazy";
|
||||
|
||||
const body = document.createElement("div");
|
||||
body.className = "card-body";
|
||||
body.innerHTML = `
|
||||
<h5 class="card-title">${room.Name}</h5>
|
||||
<p class="card-text">${room.Description}</p>
|
||||
<p class="card-text"><small>Visits: ${room.Stats?.VisitCount ?? 0}</small></p>
|
||||
`;
|
||||
|
||||
card.appendChild(img);
|
||||
card.appendChild(body);
|
||||
col.appendChild(card);
|
||||
container.appendChild(col);
|
||||
});
|
||||
} catch {
|
||||
document.getElementById("room-results").innerHTML = `<div class="banner">Failed to load rooms.</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function searchRooms() {
|
||||
const query = document.getElementById("room-search").value.trim();
|
||||
if (searchTimeout) clearTimeout(searchTimeout);
|
||||
|
||||
searchTimeout = setTimeout(async () => {
|
||||
if (!query) return loadHotRooms();
|
||||
try {
|
||||
const res = await fetchAsync(`/Room_server/rooms/search?query=${encodeURIComponent(query)}`);
|
||||
const rooms = res?.Results || [];
|
||||
const container = document.getElementById("room-results");
|
||||
container.innerHTML = "";
|
||||
if (!rooms.length) {
|
||||
container.innerHTML = `<div class="banner">No rooms found for "${query}".</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
rooms.forEach(room => {
|
||||
const col = document.createElement("div");
|
||||
col.className = "col-md-3 col-sm-6";
|
||||
|
||||
const card = document.createElement("div");
|
||||
card.className = "card";
|
||||
card.style.background = "#4a308a";
|
||||
card.style.color = "#eee";
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.src = `/img/${room.ImageName}?width=400`;
|
||||
img.className = "card-img-top";
|
||||
img.alt = room.Name;
|
||||
img.loading = "lazy";
|
||||
|
||||
const body = document.createElement("div");
|
||||
body.className = "card-body";
|
||||
body.innerHTML = `
|
||||
<h5 class="card-title">${room.Name}</h5>
|
||||
<p class="card-text">${room.Description}</p>
|
||||
<p class="card-text"><small>Visits: ${room.Stats?.VisitCount ?? 0}</small></p>
|
||||
`;
|
||||
|
||||
card.appendChild(img);
|
||||
card.appendChild(body);
|
||||
col.appendChild(card);
|
||||
container.appendChild(col);
|
||||
});
|
||||
} catch {
|
||||
document.getElementById("room-results").innerHTML = `<div class="banner">Search failed.</div>`;
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// ------------------ INIT ------------------
|
||||
AccountMe();
|
||||
setup_html();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,119 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Rec Rewild Live Admin Login</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: url('/img/RRWD_nologo.jpg') no-repeat center center fixed;
|
||||
background-size: cover;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: rgb(55, 99, 12);
|
||||
padding: 40px;
|
||||
border-radius: 1px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
box-shadow: 0 4px 6px rgb(12, 99, 76);
|
||||
backdrop-filter: blur(1px);
|
||||
}
|
||||
|
||||
.login-container h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid rgb(35, 97, 16);
|
||||
border-radius: 5px;
|
||||
background: rgb(35, 97, 16);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.input-group input:focus {
|
||||
outline: none;
|
||||
border-color: rgb(49, 146, 19);
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: #0523107e;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
background-color: rgb(35, 97, 16);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.g-recaptcha {
|
||||
margin: 15px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<h2>Login to Rewild Net</h2>
|
||||
<form action="/api/login" method="post" autocomplete="on">
|
||||
<div class="input-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="login-btn">Login</button>
|
||||
</form>
|
||||
<div class="error-message" id="error-message"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,302 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta property="og:title" content="Rec Rewild - Online Players" />
|
||||
<meta property="og:description" content="See who's currently online on Rec Rewild!" />
|
||||
<meta property="og:url" content="https://recrewild.oldrec.net" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content="https://recrewild.oldrec.net/img/DefaultRoomImage.jpg" />
|
||||
<meta name="theme-color" content="#3b2653" />
|
||||
<title>Rec Rewild — Online Players</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Poppins", Arial, sans-serif;
|
||||
background: radial-gradient(circle at 50% 20%, #0b2e1a, #000);
|
||||
color: #eaffea;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow-x: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: rgba(20, 50, 35, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(120, 255, 180, 0.2);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 0 20px rgba(80, 255, 170, 0.25);
|
||||
padding: 40px 30px;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.container:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
color: #b8ffd8;
|
||||
text-shadow: 0 0 10px #42ff9b;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1em;
|
||||
color: #cfeedd;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: linear-gradient(135deg, #1f7a4c, #42ff9b);
|
||||
border: none;
|
||||
color: #003320;
|
||||
padding: 12px 26px;
|
||||
border-radius: 10px;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.3s, transform 0.2s;
|
||||
margin-top: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
box-shadow: 0 0 15px rgba(66, 255, 155, 0.8);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.results {
|
||||
display: none;
|
||||
background: rgba(25, 70, 50, 0.8);
|
||||
padding: 25px;
|
||||
border-radius: 12px;
|
||||
margin-top: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.results.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.results h3 {
|
||||
margin-bottom: 12px;
|
||||
color: #b8ffd8;
|
||||
text-shadow: 0 0 6px #42ff9b;
|
||||
}
|
||||
|
||||
.player {
|
||||
list-style: none;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 1;
|
||||
transition: opacity 0.4s ease, background 0.2s;
|
||||
}
|
||||
|
||||
.player.fade-out {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.player img {
|
||||
border-radius: 8px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.player-display {
|
||||
font-weight: bold;
|
||||
font-size: 1.05em;
|
||||
color: #c9ffe5;
|
||||
}
|
||||
|
||||
.player-username {
|
||||
color: #7dffbf;
|
||||
text-decoration: underline;
|
||||
font-size: 0.95em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.player-room {
|
||||
color: #aadfbe;
|
||||
font-size: 0.85em;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.space-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
background: radial-gradient(circle at center, #0b2e1a, #000);
|
||||
}
|
||||
|
||||
.star {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
background: #7dffbf;
|
||||
border-radius: 50%;
|
||||
opacity: 0.8;
|
||||
animation: twinkle 3s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes twinkle {
|
||||
0%, 100% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="space-background" id="space-background"></div>
|
||||
<div class="container">
|
||||
<h1>Online Players</h1>
|
||||
<p>Currently online: <strong id="playerCount">Loading...</strong></p>
|
||||
<button onclick="showPlayers()">View Players</button>
|
||||
<div class="results" id="results">
|
||||
<h3>Online Users</h3>
|
||||
<ul id="playerList" style="padding:0;margin:0;"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const bg = document.getElementById("space-background");
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const s = document.createElement("div");
|
||||
s.className = "star";
|
||||
s.style.top = Math.random() * 100 + "%";
|
||||
s.style.left = Math.random() * 100 + "%";
|
||||
s.style.animationDelay = Math.random() * 3 + "s";
|
||||
bg.appendChild(s);
|
||||
}
|
||||
|
||||
async function updatePlayerCount() {
|
||||
try {
|
||||
const res = await fetch("https://recrewild.oldrec.net/api/online_players");
|
||||
const players = await res.json();
|
||||
document.getElementById("playerCount").textContent = players.length;
|
||||
} catch {
|
||||
document.getElementById("playerCount").textContent = "Error";
|
||||
}
|
||||
}
|
||||
|
||||
updatePlayerCount();
|
||||
setInterval(updatePlayerCount, 5000);
|
||||
|
||||
let lastPlayers = [];
|
||||
|
||||
async function fetchPlayers() {
|
||||
const res = await fetch("https://recrewild.oldrec.net/api/online_players");
|
||||
const data = await res.json();
|
||||
|
||||
if (!Array.isArray(data) || data.length === 0) return [];
|
||||
|
||||
const ids = data.map(p => p.PlayerId).join("&id=");
|
||||
const userRes = await fetch(`/Accounts/account/bulk?id=${ids}`);
|
||||
const users = await userRes.json();
|
||||
|
||||
return data.map(p => {
|
||||
const user = users.find(u => u.accountId === p.PlayerId);
|
||||
return {
|
||||
id: p.PlayerId,
|
||||
username: user?.username || "unknown",
|
||||
displayName: user?.displayName || "Unknown Player",
|
||||
displayEmoji: user?.displayEmoji || "",
|
||||
image: user?.profileImage || "DefaultImgGreen.png",
|
||||
room: p.CurrentRoom || "Unknown"
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function updatePlayerList() {
|
||||
const resultsDiv = document.getElementById("results");
|
||||
const playerList = document.getElementById("playerList");
|
||||
const header = resultsDiv.querySelector("h3");
|
||||
if (!resultsDiv.classList.contains("visible")) return;
|
||||
|
||||
try {
|
||||
const players = await fetchPlayers();
|
||||
|
||||
if (players.length === 0) {
|
||||
playerList.innerHTML = `<li style="color:#bbb;text-align:center;padding:10px;">Nobody is online 😴</li>`;
|
||||
header.style.display = "none";
|
||||
lastPlayers = [];
|
||||
return;
|
||||
} else {
|
||||
header.style.display = "block";
|
||||
}
|
||||
|
||||
const currentIds = players.map(p => p.id);
|
||||
const oldIds = lastPlayers.map(p => p.id);
|
||||
|
||||
oldIds.forEach(id => {
|
||||
if (!currentIds.includes(id)) {
|
||||
const el = document.getElementById("player-" + id);
|
||||
if (el) {
|
||||
el.classList.add("fade-out");
|
||||
setTimeout(() => el.remove(), 400);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
players.forEach(p => {
|
||||
let el = document.getElementById("player-" + p.id);
|
||||
if (!el) {
|
||||
el = document.createElement("li");
|
||||
el.className = "player";
|
||||
el.id = "player-" + p.id;
|
||||
el.style.opacity = "0";
|
||||
el.innerHTML = `
|
||||
<img src="/img/${p.image}?width=256&cropSquare=true">
|
||||
<div class="player-info">
|
||||
<div class="player-display">${p.displayName} ${p.displayEmoji}</div>
|
||||
<div class="player-username" onclick="location.href='/rewild_net/account/user/${p.username}'">@${p.username}</div>
|
||||
<div class="player-room">In: ${p.room}</div>
|
||||
</div>`;
|
||||
playerList.appendChild(el);
|
||||
setTimeout(() => el.style.opacity = "1", 10);
|
||||
} else {
|
||||
el.querySelector(".player-display").innerHTML = `${p.displayName} ${p.displayEmoji}`;
|
||||
el.querySelector(".player-room").textContent = `In: ${p.room}`;
|
||||
}
|
||||
});
|
||||
|
||||
lastPlayers = players;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
function showPlayers() {
|
||||
const resultsDiv = document.getElementById("results");
|
||||
resultsDiv.classList.add("visible");
|
||||
updatePlayerList();
|
||||
}
|
||||
|
||||
setInterval(updatePlayerList, 10000);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
Reference in New Issue
Block a user