Remove admin/server pages; consolidate live list

Delete admelist.html and serverlist.html and update the site to use lists.html as the main live-backed list. README.md was edited to reflect the consolidation (references to serverlist/admelist replaced with lists.html and roulette.html, wording adjusted). lists.html: remove admin/server links, change page title to “FEDL List”, and drop the level column in the table. app.js: update status text to mention the live server list, remove admin editor code and serverlist-specific branches, simplify list rendering (remove level column output) and wire live updates to listPage.applyItems. Miscellaneous: remove unused escapeAttr helper. Overall this simplifies the UI by consolidating live functionality into lists.html and removing the separate admin/server pages.
This commit is contained in:
2026-03-31 19:00:35 -05:00
parent 906ef15e4b
commit d29719131c
5 changed files with 17 additions and 329 deletions
+10 -29
View File
@@ -1,22 +1,18 @@
# GD FEDL
A Geometry Dash FEDL-style website with both a static list and a live editable server list.
A Geometry Dash FEDL-style website with a live server-backed main list.
It includes:
- a home page
- a static list page
- a live server-backed list page
- an admin editor page for the live list
- a main list page
- a players page
- a rules page
## Project Files
- `index.html` - home page
- `lists.html` - static list view
- `serverlist.html` - live server-backed list view
- `admelist.html` - admin UI for editing the live list
- `lists.html` - main live list view
- `players.html` - player manager
- `rules.html` - submission rules and mod guidelines
- `app.js` - client-side logic
@@ -43,9 +39,8 @@ new|1|Flamewall|https://youtu.be/x4Io4zkWVRw
## Pages
- `lists.html` reads the static list.
- `serverlist.html` reads the live server list.
- `admelist.html` lets you edit the live server list with a UI.
- `lists.html` is the main live list page.
- `roulette.html` uses the same live server-backed list.
## Fallback Order
@@ -55,7 +50,7 @@ The live list tries these in order:
2. `server/data.txt`
3. `data.txt`
That means `serverlist.html` can still show data even if the live API is down.
That means `lists.html` can still show data even if the live API is down.
## Running The Static Site
@@ -73,7 +68,7 @@ http://localhost:8000
## Running The Live Server
The live editor and live list use the Node server:
The live list uses the Node server:
```bash
node server/server.js
@@ -88,8 +83,8 @@ HOST=0.0.0.0 PORT=3000 node server/server.js
Then open:
```txt
http://localhost:3000/serverlist.html
http://localhost:3000/admelist.html
http://localhost:3000/lists.html
http://localhost:3000/roulette.html
```
On macOS you can also use:
@@ -98,27 +93,13 @@ On macOS you can also use:
start-server.command
```
## Editing The Live List
Open `admelist.html` through the Node server.
Features:
- add a new row at the top as a draft
- give it a number when you want to place it
- automatic shifting of other rows when a number is reused
- delete rows only with the Delete button
- live refresh on connected list pages after saving
Draft rows must be assigned a number before saving.
## Hosting On Another Device
If you want one device to host the live list for other devices:
1. Run the server with `HOST=0.0.0.0`.
2. Open or forward port `3000`.
3. Visit `http://YOUR-IP:3000/serverlist.html` or `http://YOUR-IP:3000/admelist.html`.
3. Visit `http://YOUR-IP:3000/lists.html` or `http://YOUR-IP:3000/roulette.html`.
If you already have router port forwarding set up, point it to the machine running `server/server.js`.
-52
View File
@@ -1,52 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Edit List - GD fedl</title>
<link rel="stylesheet" href="styles.css">
</head>
<body data-page="admelist">
<header>
<h1>fedl</h1>
<nav>
<a href="index.html">Home</a>
<a href="serverlist.html">Server List</a>
<a href="lists.html">Static List</a>
<a href="players.html">Players</a>
</nav>
</header>
<main class="admin-shell">
<section class="panel admin-panel">
<div class="admin-toolbar">
<div>
<p class="hero-kicker">Editor</p>
<h2>Manage the live list</h2>
<p id="admin-status" class="muted">Loading list data...</p>
</div>
<div class="admin-actions">
<input id="admin-search" type="text" placeholder="Search rows..." />
<button id="add-row" type="button" class="btn ghost-btn">Add Row</button>
<button id="save-list" type="button" class="btn">Save List</button>
</div>
</div>
<div class="table-wrap">
<table class="levels-table admin-table">
<thead>
<tr>
<th style="width:90px">#</th>
<th style="width:140px">Level</th>
<th>Title</th>
<th>Video URL</th>
<th style="width:120px">Delete</th>
</tr>
</thead>
<tbody id="admin-list-body"></tbody>
</table>
</div>
</section>
</main>
<script src="app.js"></script>
</body>
</html>
+5 -196
View File
@@ -172,7 +172,7 @@
Promise.all([loadItems(), loadLevelMeta()]).then(([items, metaMap])=>{
if(!items.length){
statusEl.textContent = 'No demons found.';
titleEl.textContent = 'Add demons to server/data.txt';
titleEl.textContent = 'Add demons to the live server list';
idEl.textContent = 'Level ID: -';
noteEl.textContent = 'No list data was found.';
return;
@@ -196,7 +196,7 @@
titleEl.textContent = 'Run the site on a local server';
rankEl.textContent = 'Rank: -';
idEl.textContent = 'Level ID: -';
noteEl.textContent = 'The local file or API lookup failed.';
noteEl.textContent = 'The live list or API lookup failed.';
console.error(err);
});
});
@@ -295,12 +295,11 @@
const tr = document.createElement('tr');
const tdNum = document.createElement('td'); tdNum.textContent = it.position;
const tdTitle = document.createElement('td'); tdTitle.textContent = it.title;
const tdLevel = document.createElement('td'); tdLevel.textContent = it.level;
const tdAct = document.createElement('td');
const a = document.createElement('a'); a.textContent='Open'; a.href='#'; a.className='btn';
a.addEventListener('click', (e)=>{e.preventDefault(); openVideo(it.url)});
tdAct.appendChild(a);
tr.appendChild(tdNum); tr.appendChild(tdTitle); tr.appendChild(tdLevel); tr.appendChild(tdAct);
tr.appendChild(tdNum); tr.appendChild(tdTitle); tr.appendChild(tdAct);
tbody.appendChild(tr);
});
}
@@ -343,204 +342,14 @@
}
// Lists page
if(page==='lists' || page==='serverlist'){
if(page==='lists'){
const listPage = initListPage();
if(page==='serverlist'){
bindLiveUpdates();
onLiveUpdate(function(items){
const status = qs('live-status');
if(status){
status.textContent = 'Live data updated';
}
listPage.applyItems(items);
});
}
}
if(page==='admelist'){
const statusEl = qs('admin-status');
const tbody = qs('admin-list-body');
const addBtn = qs('add-row');
const saveBtn = qs('save-list');
const searchEl = qs('admin-search');
let items = [];
function setStatus(message, isError){
if(!statusEl) return;
statusEl.textContent = message;
statusEl.classList.toggle('error-text', !!isError);
}
function filteredItems(){
const query = (searchEl && searchEl.value || '').trim().toLowerCase();
if(!query) return items;
return items.filter(item=>{
return [item.level, item.position, item.title, item.url].some(value=>
String(value || '').toLowerCase().includes(query)
);
});
}
function normalizePositions(){
let position = 1;
items.forEach(item=>{
if(item._isDraft){
item.position = '';
return;
}
item.position = String(position);
position += 1;
});
}
function moveItemToPosition(index, rawPosition){
if(!items[index]) return;
const parsedPosition = Number(rawPosition);
if(!Number.isFinite(parsedPosition) || parsedPosition < 1) return;
const nextPosition = Math.max(1, parsedPosition);
const [item] = items.splice(index, 1);
item._isDraft = false;
const drafts = items.filter(entry=>entry._isDraft);
const ranked = items.filter(entry=>!entry._isDraft);
const targetIndex = Math.min(ranked.length, nextPosition - 1);
ranked.splice(targetIndex, 0, item);
items = drafts.concat(ranked);
normalizePositions();
}
function renderAdminTable(){
const rows = filteredItems();
tbody.innerHTML = '';
rows.forEach((item, index)=>{
const actualIndex = items.indexOf(item);
const tr = document.createElement('tr');
tr.innerHTML = `
<td><input data-field="position" data-index="${actualIndex}" type="number" min="1" value="${escapeAttr(item.position)}"></td>
<td><input data-field="level" data-index="${actualIndex}" type="text" value="${escapeAttr(item.level)}"></td>
<td><input data-field="title" data-index="${actualIndex}" type="text" value="${escapeAttr(item.title)}"></td>
<td><input data-field="url" data-index="${actualIndex}" type="url" value="${escapeAttr(item.url)}"></td>
<td><button type="button" class="btn danger-btn small-btn" data-delete="${actualIndex}">Delete</button></td>
`;
tbody.appendChild(tr);
});
if(!rows.length){
const tr = document.createElement('tr');
tr.innerHTML = '<td colspan="5" class="muted">No rows match your search.</td>';
tbody.appendChild(tr);
}
}
function saveItems(){
const hasUnplacedDraft = items.some(item=>{
const hasContent = String(item.title || '').trim() || String(item.url || '').trim() || String(item.level || '').trim();
return item._isDraft && hasContent;
});
if(hasUnplacedDraft){
setStatus('Give each new row a number before saving.', true);
return Promise.resolve();
}
items = items
.map(item=>({
level: String(item.level || '').trim() || 'new',
position: String(item.position || '').trim(),
title: String(item.title || '').trim(),
url: String(item.url || '').trim()
}))
.filter(item=>item.title);
normalizePositions();
return fetch(liveApiUrl, {
method:'PUT',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({text: formatData(items)})
}).then(r=>{
if(!r.ok) throw new Error('Save failed');
clearItemsCache();
renderAdminTable();
setStatus('Saved. Live pages update automatically.');
}).catch(err=>{
console.error(err);
setStatus('Could not save. Start the Node server and try again.', true);
});
}
function loadAdmin(){
loadItems().then(loaded=>{
items = loaded.slice().sort((a,b)=>(Number(a.position) || 0) - (Number(b.position) || 0)).map(item=>({
level: item.level,
position: item.position,
title: item.title,
url: item.url,
_isDraft: false
}));
normalizePositions();
renderAdminTable();
setStatus('Connected to live list data.');
}).catch(err=>{
console.error(err);
setStatus('Could not load list data. Start the Node server first.', true);
});
}
tbody.addEventListener('input', function(event){
const target = event.target;
const field = target.getAttribute('data-field');
const index = Number(target.getAttribute('data-index'));
if(!field || Number.isNaN(index) || !items[index]) return;
if(field === 'position'){
moveItemToPosition(index, target.value);
renderAdminTable();
}else{
items[index][field] = target.value;
}
setStatus('Unsaved changes');
});
tbody.addEventListener('click', function(event){
const deleteButton = event.target.closest('[data-delete]');
if(!deleteButton) return;
const deleteIndex = deleteButton.getAttribute('data-delete');
if(deleteIndex == null) return;
const index = Number(deleteIndex);
if(Number.isNaN(index)) return;
items.splice(index, 1);
normalizePositions();
renderAdminTable();
setStatus('Row removed. Save when ready.');
});
addBtn.addEventListener('click', function(){
items.unshift({level:'new', position:'', title:'', url:'', _isDraft:true});
normalizePositions();
renderAdminTable();
setStatus('New row added at the top. Give it a number when you want to place it.');
});
saveBtn.addEventListener('click', function(){
saveItems();
});
if(searchEl){
searchEl.addEventListener('input', renderAdminTable);
}
bindLiveUpdates();
onLiveUpdate(function(updatedItems){
items = updatedItems.slice().sort((a,b)=>(Number(a.position) || 0) - (Number(b.position) || 0)).map(item=>({
level: item.level,
position: item.position,
title: item.title,
url: item.url,
_isDraft: false
}));
normalizePositions();
renderAdminTable();
setStatus('List reloaded from live server.');
listPage.applyItems(updatedItems);
});
loadAdmin();
}
// Utility
function escapeHtml(s){return String(s).replace(/[&<>"']/g, c=>({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"})[c])}
function escapeAttr(s){return escapeHtml(String(s == null ? '' : s))}
})();
+2 -4
View File
@@ -13,8 +13,6 @@
<a href="index.html">Home</a>
<a href="players.html">Players</a>
<a href="rules.html">Rules</a>
<a href="serverlist.html">Server List</a>
<a href="admelist.html">Edit List</a>
<a href="roulette.html">Roulette</a>
</nav>
</header>
@@ -28,7 +26,7 @@
<section class="panel content">
<div class="search-row">
<h2 id="list-title">All Videos</h2>
<h2 id="list-title">FEDL List</h2>
<input id="search" type="text" placeholder="Search videos or titles..." />
<select id="level-filter"><option value="all">All Levels</option></select>
</div>
@@ -36,7 +34,7 @@
<div class="table-wrap">
<table class="levels-table">
<thead>
<tr><th style="width:80px">#</th><th>Title</th><th style="width:120px">Level</th><th style="width:140px">Action</th></tr>
<tr><th style="width:80px">#</th><th>Title</th><th style="width:140px">Action</th></tr>
</thead>
<tbody id="list-area"></tbody>
</table>
-48
View File
@@ -1,48 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Server List - GD fedl</title>
<link rel="stylesheet" href="styles.css">
</head>
<body data-page="serverlist">
<header>
<h1>fedl</h1>
<nav>
<a href="index.html">Home</a>
<a href="lists.html">Static List</a>
<a href="admelist.html">Edit List</a>
<a href="players.html">Players</a>
<a href="roulette.html">Roulette</a>
</nav>
</header>
<main class="layout">
<aside class="sidebar">
<div class="panel">
<h2>Live Ranges</h2>
<p id="live-status" class="muted">Connected to live server data</p>
<ul id="levels"></ul>
</div>
</aside>
<section class="panel content">
<div class="search-row">
<h2 id="list-title">Live Server List</h2>
<input id="search" type="text" placeholder="Search videos or titles..." />
<select id="level-filter"><option value="all">Full List</option></select>
</div>
<div class="table-wrap">
<table class="levels-table">
<thead>
<tr><th style="width:80px">#</th><th>Title</th><th style="width:120px">Level</th><th style="width:140px">Action</th></tr>
</thead>
<tbody id="list-area"></tbody>
</table>
</div>
</section>
</main>
<script src="app.js"></script>
</body>
</html>