Add initial implementation of GD Demand List application with multi-page support

This commit is contained in:
2026-03-28 00:23:52 -05:00
parent f1c95e36d7
commit f8fcec309b
6 changed files with 202 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
// Lightweight multi-page handler for GD Demand List
(function(){
function qs(id){return document.getElementById(id)}
const page = document.body.dataset.page;
// Storage helpers
function read(key, fallback){
try{const v = localStorage.getItem(key); return v?JSON.parse(v):fallback}
catch(e){return fallback}
}
function write(key, val){localStorage.setItem(key,JSON.stringify(val))}
// Players page
if(page==='players'){
const form = qs('player-form'); const nameIn = qs('player-name'); const list = qs('players');
let players = read('gd_players',[]);
function render(){list.innerHTML=''; players.forEach((p,idx)=>{
const li=document.createElement('li'); li.innerHTML=`<span><span class="name">${escapeHtml(p.name)}</span> <span class="muted">(${p.id})</span></span>`;
const actions=document.createElement('div'); actions.className='actions';
const edit=document.createElement('button'); edit.textContent='Edit'; edit.onclick=()=>{const nv=prompt('Edit name',p.name); if(nv){players[idx].name=nv; write('gd_players',players); render()}};
const del=document.createElement('button'); del.textContent='Delete'; del.onclick=()=>{if(confirm('Delete player?')){players.splice(idx,1); write('gd_players',players); render()}};
actions.appendChild(edit); actions.appendChild(del); li.appendChild(actions); list.appendChild(li);
})}
form.addEventListener('submit',e=>{e.preventDefault(); const name=nameIn.value.trim(); if(!name) return; players.push({id:Date.now().toString(36),name}); write('gd_players',players); nameIn.value=''; render()});
render();
}
// Lists page
if(page==='lists'){
const levelsEl = qs('levels'); const listArea = qs('list-area'); const titleEl = qs('list-title');
// Load hard-coded data file data.txt (level|position|title|url per line)
function loadData(){
fetch('data.txt').then(r=>r.text()).then(txt=>{
const lines = txt.split(/\r?\n/).map(l=>l.trim()).filter(Boolean);
const items = lines.map(l=>{
const parts = l.split('|').map(p=>p.trim());
return {level:parts[0]||'Unknown',position:parts[1]||'',title:parts[2]||'Untitled',url:parts[3]||''};
});
setupLevels(items);
}).catch(err=>{listArea.innerHTML='<p class="muted">Failed to load data.txt — run via a local server.</p>'; console.error(err)});
}
function setupLevels(items){
const levels = [...new Set(items.map(i=>i.level))];
levelsEl.innerHTML=''; levels.forEach(l=>{
const a=document.createElement('a'); a.href='#'; a.textContent=l; a.className='level-link'; a.onclick=(e)=>{e.preventDefault(); selectLevel(l,items,a)}; levelsEl.appendChild(a);
});
// default select first
if(levels.length) selectLevel(levels[0],items,levelsEl.querySelector('.level-link'));
}
function selectLevel(level, items, linkEl){
// mark active
levelsEl.querySelectorAll('.level-link').forEach(a=>a.classList.remove('active'));
if(linkEl) linkEl.classList.add('active');
titleEl.textContent = level + ' Videos';
const filtered = items.filter(it=>it.level===level).sort((a,b)=> (a.position||0)-(b.position||0));
listArea.innerHTML = '';
filtered.forEach(it=>{
const card = document.createElement('div'); card.className='video-card';
const h = document.createElement('h3'); h.textContent = `${it.position}${it.title}`;
card.appendChild(h);
const url = it.url;
if(/youtube\.com|youtu\.be/.test(url)){
const id = extractYouTubeID(url);
const iframe = document.createElement('iframe'); iframe.src = `https://www.youtube.com/embed/${id}`; iframe.allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'; iframe.allowFullscreen = true;
card.appendChild(iframe);
} else if(url){
const a = document.createElement('a'); a.href = url; a.textContent = url; a.target='_blank'; card.appendChild(a);
} else {
const p = document.createElement('p'); p.textContent = 'No URL'; card.appendChild(p);
}
listArea.appendChild(card);
});
}
function extractYouTubeID(url){
const m = url.match(/(?:v=|\/embed\/|youtu\.be\/)([A-Za-z0-9_-]{6,})/); return m?m[1]:'';
}
loadData();
}
// Utility
function escapeHtml(s){return String(s).replace(/[&<>"']/g, c=>({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"})[c])}
})();
+7
View File
@@ -0,0 +1,7 @@
Easy|1|Beginner Tutorial|https://www.youtube.com/watch?v=dQw4w9WgXcQ
Easy|2|Simple Level Showcase|https://www.youtube.com/watch?v=V-_O7nl0Ii0
Normal|1|Intermediate Run|https://www.youtube.com/watch?v=3JZ_D3ELwOQ
Hard|1|Hard Demon Playthrough|https://www.youtube.com/watch?v=fJ9rUzIMcZQ
Insane|1|Insane Demon Compilation|https://www.youtube.com/watch?v=L_jWHffIx5E
new|1|Insane Demon Compilation|https://www.youtube.com/watch?v=L_jWHffIx5E
cool|1|Insane Demon Compilation|https://www.youtube.com/watch?v=L_jWHffIx5E
+22
View File
@@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>GD Demand List</title>
<link rel="stylesheet" href="styles.css">
</head>
<body data-page="index">
<header>
<h1>GD Demand List</h1>
<nav>
<a href="players.html">Players</a>
<a href="lists.html">Lists</a>
</nav>
</header>
<main>
<p>Welcome — choose a page above to manage players or demand lists.</p>
</main>
<script src="app.js"></script>
</body>
</html>
+30
View File
@@ -0,0 +1,30 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Lists — GD Demand List</title>
<link rel="stylesheet" href="styles.css">
</head>
<body data-page="lists">
<header>
<h1>Demand Lists</h1>
<nav>
<a href="index.html">Home</a>
<a href="players.html">Players</a>
</nav>
</header>
<main class="layout">
<aside class="sidebar panel">
<h2>Levels</h2>
<ul id="levels"></ul>
</aside>
<section class="panel content">
<h2 id="list-title">All Videos</h2>
<div id="list-area"></div>
</section>
</main>
<script src="app.js"></script>
</body>
</html>
+33
View File
@@ -0,0 +1,33 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Players — GD Demand List</title>
<link rel="stylesheet" href="styles.css">
</head>
<body data-page="players">
<header>
<h1>Players</h1>
<nav>
<a href="index.html">Home</a>
<a href="lists.html">Lists</a>
</nav>
</header>
<main>
<section class="panel">
<h2>Add Player</h2>
<form id="player-form">
<input id="player-name" placeholder="Player name" required />
<button type="submit">Add</button>
</form>
</section>
<section class="panel">
<h2>Players</h2>
<ul id="players"></ul>
</section>
</main>
<script src="app.js"></script>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
:root{--bg:#0f1724;--panel:#0b1220;--accent:#ff6b6b;--muted:#94a3b8;--text:#e6eef8}
*{box-sizing:border-box}
html,body{height:100%;margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,"Helvetica Neue",Arial;color:var(--text);background:linear-gradient(180deg,#061021 0%,#071426 100%)}
header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px}
header h1{margin:0;font-size:1.2rem}
nav a{color:var(--muted);margin-left:12px;text-decoration:none}
main{padding:16px 24px}
.layout{display:flex;gap:12px}
.sidebar{width:200px}
.content{flex:1}
.panel{background:rgba(255,255,255,0.02);padding:12px;border-radius:8px;margin-bottom:12px}
form input{padding:8px;border-radius:6px;border:1px solid rgba(255,255,255,0.06);margin-right:8px;color:var(--text);background:transparent}
button{background:var(--accent);color:#fff;border:none;padding:8px 12px;border-radius:6px}
ul{list-style:none;padding:0;margin:0}
li{display:flex;align-items:center;justify-content:space-between;padding:8px;border-bottom:1px solid rgba(255,255,255,0.02)}
.actions button{margin-left:6px;background:transparent;border:1px solid rgba(255,255,255,0.06);color:var(--muted);padding:6px;border-radius:6px}
.name{font-weight:600}
.muted{color:var(--muted);font-size:0.9rem}
.video-card{margin-bottom:12px;padding:10px;border-radius:8px;background:rgba(255,255,255,0.02)}
.video-card iframe{width:100%;height:320px;border:0;border-radius:6px}
.level-link{display:block;padding:6px 8px;border-radius:6px;color:var(--muted);text-decoration:none}
.level-link.active{background:rgba(255,255,255,0.03);color:var(--text)}