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:
@@ -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`.
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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=>({"&":"&","<":"<",">":">","\"":""","'":"'"})[c])}
|
||||
function escapeAttr(s){return escapeHtml(String(s == null ? '' : s))}
|
||||
})();
|
||||
|
||||
+2
-4
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user