feat: full de-Angularization with Bootstrap 5 migration
- Removed AngularJS 1.4.8 completely — no more framework dependency - Extracted core RCON logic into pure vanilla JS (js/rcon.js) - Added lightweight hash-based SPA router (js/router.js) - Rewrote all 6 pages as vanilla JS modules in js/pages/ - Migrated Bootstrap 3 → Bootstrap 5 (cards instead of panels, bootstrap-icons instead of glyphicons) - Removed jQuery dependency (Bootstrap 5 is vanilla JS) - Purged all Angular templates from html/ - Removed npm artifacts (package.json, node_modules) - Added nginx.example.conf for static hosting - Updated rcon.css dark theme for BS5 variables - Static file only — no Node.js backend required Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Player list page — table with sort, filter, and kick.
|
||||
*/
|
||||
function PlayerListPage(params, container) {
|
||||
var address = Rcon.getAddress() || '';
|
||||
|
||||
var html =
|
||||
'<div id="PlayerListController">' +
|
||||
'<div class="row mb-3">' +
|
||||
'<div class="col-6">' +
|
||||
'<input id="player-filter" class="form-control" placeholder="Filter..">' +
|
||||
'</div>' +
|
||||
'<div class="col-6">' +
|
||||
'<button id="player-refresh" class="btn btn-secondary float-end" type="button"><i class="bi bi-arrow-clockwise"></i></button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="row">' +
|
||||
'<div class="col-sm-12">' +
|
||||
'<div class="table-responsive">' +
|
||||
'<table class="table" id="player-table">' +
|
||||
'<thead>' +
|
||||
'<tr>' +
|
||||
'<th>Actions</th>' +
|
||||
'<th class="sortable" data-field="DisplayName">Username</th>' +
|
||||
'<th class="sortable text-end" data-field="SteamID">Steam ID</th>' +
|
||||
'<th class="sortable text-end" data-field="VoiationLevel">Violation Level</th>' +
|
||||
'<th class="sortable text-end" data-field="Address">Address</th>' +
|
||||
'<th class="sortable text-end" data-field="Ping">Ping</th>' +
|
||||
'<th class="sortable text-end" data-field="ConnectedSeconds">Duration</th>' +
|
||||
'</tr>' +
|
||||
'</thead>' +
|
||||
'<tbody id="player-tbody"></tbody>' +
|
||||
'</table>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
container.innerHTML = html;
|
||||
|
||||
var players = [];
|
||||
var orderBy = '-ConnectedSeconds';
|
||||
var filterText = '';
|
||||
|
||||
function sortPlayers(a, b) {
|
||||
var desc = orderBy[0] === '-';
|
||||
var field = desc ? orderBy.substring(1) : orderBy;
|
||||
var va = a[field], vb = b[field];
|
||||
if (typeof va === 'string') va = va.toLowerCase();
|
||||
if (typeof vb === 'string') vb = vb.toLowerCase();
|
||||
if (va < vb) return desc ? 1 : -1;
|
||||
if (va > vb) return desc ? -1 : 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function matchesFilter(p) {
|
||||
if (!filterText) return true;
|
||||
var ft = filterText.toLowerCase();
|
||||
return (p.DisplayName && p.DisplayName.toLowerCase().indexOf(ft) !== -1) ||
|
||||
(p.SteamID && p.SteamID.indexOf(ft) !== -1) ||
|
||||
(p.Address && p.Address.toLowerCase().indexOf(ft) !== -1);
|
||||
}
|
||||
|
||||
function render() {
|
||||
var filtered = players.filter(matchesFilter).sort(sortPlayers);
|
||||
var tbody = document.getElementById('player-tbody');
|
||||
var html = '';
|
||||
for (var i = 0; i < filtered.length; i++) {
|
||||
var p = filtered[i];
|
||||
var fadeClass = (p.VoiationLevel === 0) ? ' class="fade"' : '';
|
||||
html +=
|
||||
'<tr>' +
|
||||
'<td>' +
|
||||
'<div class="btn-group">' +
|
||||
'<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">' +
|
||||
'<i class="bi bi-gear-fill"></i>' +
|
||||
'</button>' +
|
||||
'<ul class="dropdown-menu">' +
|
||||
'<li><a class="dropdown-item" href="http://www.steamcommunity.com/profiles/' + p.SteamID + '" target="_blank"><i class="bi bi-info-circle-fill"></i> Steam Profile</a></li>' +
|
||||
'<li><hr class="dropdown-divider"></li>' +
|
||||
'<li><a class="dropdown-item kick-player" href="#" data-id="' + p.SteamID + '"><i class="bi bi-trash-fill"></i> Kick</a></li>' +
|
||||
'</ul>' +
|
||||
'</div>' +
|
||||
'</td>' +
|
||||
'<td><a href="#/' + address + '/player/' + p.SteamID + '/">' + (p.DisplayName || '') + '</a></td>' +
|
||||
'<td class="text-end"><a href="#/' + address + '/player/' + p.SteamID + '/">' + p.SteamID + '</a></td>' +
|
||||
'<td class="text-end"' + fadeClass + '>' + (p.VoiationLevel || 0) + '</td>' +
|
||||
'<td class="text-end">' + (p.Address || '') + '</td>' +
|
||||
'<td class="text-end">' + (p.Ping || 0) + '</td>' +
|
||||
'<td class="text-end">' + secondsToDuration(p.ConnectedSeconds || 0) + '</td>' +
|
||||
'</tr>';
|
||||
}
|
||||
tbody.innerHTML = html;
|
||||
|
||||
// Bind kick buttons
|
||||
var kicks = tbody.querySelectorAll('.kick-player');
|
||||
for (var j = 0; j < kicks.length; j++) {
|
||||
kicks[j].addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
Rcon.command('kick ' + this.dataset.id);
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
Rcon.getPlayers(function (list) {
|
||||
players = list;
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
function updateSortHeaders() {
|
||||
var headers = document.querySelectorAll('#player-table .sortable');
|
||||
for (var i = 0; i < headers.length; i++) {
|
||||
headers[i].className = headers[i].className.replace(/sorting(\s+descending)?/g, '').trim();
|
||||
var field = headers[i].dataset.field;
|
||||
if (orderBy === field) headers[i].className += ' sorting';
|
||||
else if (orderBy === '-' + field) headers[i].className += ' sorting descending';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('player-refresh').addEventListener('click', refresh);
|
||||
document.getElementById('player-filter').addEventListener('input', function () {
|
||||
filterText = this.value;
|
||||
render();
|
||||
});
|
||||
|
||||
// Sort header clicks
|
||||
var sortHeaders = document.querySelectorAll('#player-table .sortable');
|
||||
for (var k = 0; k < sortHeaders.length; k++) {
|
||||
sortHeaders[k].addEventListener('click', function () {
|
||||
var field = this.dataset.field;
|
||||
orderBy = (orderBy === field) ? '-' + field : field;
|
||||
updateSortHeaders();
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
if (Rcon.isConnected()) { refresh(); updateSortHeaders(); }
|
||||
else { var _c = function () { refresh(); updateSortHeaders(); Rcon.off('connected', _c); }; Rcon.on('connected', _c); }
|
||||
}
|
||||
Reference in New Issue
Block a user