diff --git a/README.md b/README.md index 87f5a3d..1b48dd1 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,5 @@ Features: ToDo: -- move interval button to a neutral spot -- sections to list faction memebers - - needs to be movable objects - Assignment pools depending on stats - - players will be round-robin queued their targets from here \ No newline at end of file + - players will be round-robin queued their targets from here diff --git a/static/dashboard.js b/static/dashboard.js index b1e5011..3fa7858 100644 --- a/static/dashboard.js +++ b/static/dashboard.js @@ -1,134 +1,156 @@ // dashboard.js +// Full functionality: populate, start status loops, load members, drag & drop between lists and groups, +// drop zones accept only friendly/enemy respectively, status color updates. -const friendlyMembers = new Map(); +// ---- In-memory maps ---- +const friendlyMembers = new Map(); // id -> member object const enemyMembers = new Map(); +// ---- DOM containers ---- const friendlyContainer = document.getElementById("friendly-container"); const enemyContainer = document.getElementById("enemy-container"); -function createMemberCard(member) { +// utility to safe parse ints +function toInt(v) { const n = Number(v); return Number.isNaN(n) ? null : n; } + +// ---- Create structured card ---- +function createMemberCard(member, kind) { + // member: { id, name, level, estimate, status } + // kind: "friendly" or "enemy" (for ARIA / dataset) const card = document.createElement("div"); card.classList.add("member-card"); + card.classList.add(kind); + card.setAttribute("draggable", "true"); card.dataset.id = member.id; + card.dataset.kind = kind; - // Clear innerHTML, create structured divs + // structured children const nameDiv = document.createElement("div"); - nameDiv.classList.add("name"); + nameDiv.className = "name"; nameDiv.textContent = member.name; const statsDiv = document.createElement("div"); - statsDiv.classList.add("stats"); - statsDiv.innerHTML = ` - Level: ${member.level}
- Estimate: ${member.estimate}
- Status: ${member.status || "Unknown"} - `; + statsDiv.className = "stats"; + statsDiv.innerHTML = `Lv: ${member.level}
Est: ${member.estimate}
Status: ${member.status || "Unknown"}`; card.appendChild(nameDiv); card.appendChild(statsDiv); - // Store reference to DOM element + // store back-reference member.domElement = card; - // Make card draggable - card.draggable = true; - card.addEventListener("dragstart", e => { - e.dataTransfer.setData("text/plain", member.id); + // drag events: encode type and id in dataTransfer + card.addEventListener("dragstart", (e) => { + const payload = JSON.stringify({ kind, id: member.id }); + e.dataTransfer.setData("text/plain", payload); + // visual + card.style.opacity = "0.5"; + }); + card.addEventListener("dragend", () => { + card.style.opacity = "1"; }); - // Set initial status color - updateStatusColor(member); + // initial color + applyStatusClass(member); return card; } -function updateStatusColor(member) { - const statusSpan = member.domElement.querySelector(".status-text"); - statusSpan.classList.remove("status-ok", "status-traveling", "status-hospitalized"); - - if (!member.status) return; - - const s = member.status.toLowerCase(); - if (s === "okay") statusSpan.classList.add("status-ok"); - else if (s === "traveling" || s === "abroad") statusSpan.classList.add("status-traveling"); - else if (s === "hospitalized") statusSpan.classList.add("status-hospitalized"); +// ---- Apply status CSS class to the .status-text inside a member card ---- +function applyStatusClass(member) { + const span = member.domElement?.querySelector(".status-text"); + if (!span) return; + span.classList.remove("status-ok", "status-traveling", "status-hospitalized"); + const s = String(member.status || "").toLowerCase(); + if (s === "okay") span.classList.add("status-ok"); + else if (s === "traveling" || s === "abroad") span.classList.add("status-traveling"); + else if (s === "hospitalized" || s === "hospital") span.classList.add("status-hospitalized"); } +// ---- Load static members from backend into the list containers ---- async function loadMembers(faction) { const url = faction === "friendly" ? "/api/friendly_members" : "/api/enemy_members"; - const response = await fetch(url); - const members = await response.json(); - - const container = faction === "friendly" ? friendlyContainer : enemyContainer; - const map = faction === "friendly" ? friendlyMembers : enemyMembers; - - container.innerHTML = ""; - map.clear(); - - members.forEach(m => { - if (!m.status) m.status = "Unknown"; - const card = createMemberCard(m); - map.set(m.id, m); - container.appendChild(card); - }); - - refreshStatus(faction); -} - -async function refreshStatus(faction) { - const url = faction === "friendly" ? "/api/friendly_status" : "/api/enemy_status"; - const map = faction === "friendly" ? friendlyMembers : enemyMembers; - try { - const response = await fetch(url); - const statusData = await response.json(); + const res = await fetch(url, { cache: "no-store" }); + if (!res.ok) { + console.error("Failed to fetch members:", res.status); + return; + } + const arr = await res.json(); + const container = faction === "friendly" ? friendlyContainer : enemyContainer; + const map = faction === "friendly" ? friendlyMembers : enemyMembers; - Object.keys(statusData).forEach(id => { - const member = map.get(parseInt(id)); - if (!member) return; - member.status = statusData[id].status; + // clear + container.innerHTML = ""; + map.clear(); - // Update DOM - const statusSpan = member.domElement.querySelector(".status-text"); - statusSpan.textContent = member.status; - - // Apply correct color class - updateStatusColor(member); + arr.forEach(m => { + if (!m.status) m.status = "Unknown"; + const card = createMemberCard(m, faction); + map.set(m.id, m); + container.appendChild(card); }); } catch (err) { - console.error("Failed to refresh status:", err); + console.error("loadMembers error", err); } } -async function populateFriendly() { - const id = parseInt(document.getElementById("friendly-id").value); - if (!id) return alert("Enter a valid faction ID!"); +// ---- Refresh status map and update DOM (does not touch static info or position) ---- +async function refreshStatus(faction) { + const url = faction === "friendly" ? "/api/friendly_status" : "/api/enemy_status"; + const map = faction === "friendly" ? friendlyMembers : enemyMembers; + try { + const res = await fetch(url, { cache: "no-store" }); + if (!res.ok) { + console.warn("Status fetch failed:", res.status); + return; + } + const statusData = await res.json(); // { id: { status: "..."}, ... } + Object.keys(statusData).forEach(k => { + const id = parseInt(k); + const member = map.get(id); + if (!member) return; // not loaded yet or moved elsewhere + member.status = statusData[k].status; + const span = member.domElement.querySelector(".status-text"); + span.textContent = member.status; + applyStatusClass(member); + }); + } catch (err) { + console.error("refreshStatus error", err); + } +} +// ---- Populate endpoints: call backend to fetch static info (FFScouter) once ---- +async function populateFriendly() { + const id = toInt(document.getElementById("friendly-id").value); + if (!id) return alert("Enter friendly faction ID"); await fetch("/api/populate_friendly", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ faction_id: id, interval: 0 }) }); - - loadMembers("friendly"); + await loadMembers("friendly"); } async function populateEnemy() { - const id = parseInt(document.getElementById("enemy-id").value); - if (!id) return alert("Enter a valid faction ID!"); - + const id = toInt(document.getElementById("enemy-id").value); + if (!id) return alert("Enter enemy faction ID"); await fetch("/api/populate_enemy", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ faction_id: id, interval: 0 }) }); - - loadMembers("enemy"); + await loadMembers("enemy"); } +// ---- Start status refresh loops on backend and JS-side polling ---- +let friendlyStatusIntervalHandle = null; +let enemyStatusIntervalHandle = null; + async function startFriendlyStatus() { - const id = parseInt(document.getElementById("friendly-id").value); - const interval = parseInt(document.getElementById("refresh-interval").value) || 10; + const id = toInt(document.getElementById("friendly-id").value); + const interval = Math.max(1, toInt(document.getElementById("refresh-interval").value) || 10); + if (!id) return alert("Enter friendly faction ID"); await fetch("/api/start_friendly_status", { method: "POST", @@ -136,12 +158,17 @@ async function startFriendlyStatus() { body: JSON.stringify({ faction_id: id, interval }) }); - setInterval(() => refreshStatus("friendly"), interval * 1000); + // clear existing + if (friendlyStatusIntervalHandle) clearInterval(friendlyStatusIntervalHandle); + friendlyStatusIntervalHandle = setInterval(() => refreshStatus("friendly"), interval * 1000); + // immediate + refreshStatus("friendly"); } async function startEnemyStatus() { - const id = parseInt(document.getElementById("enemy-id").value); - const interval = parseInt(document.getElementById("refresh-interval").value) || 10; + const id = toInt(document.getElementById("enemy-id").value); + const interval = Math.max(1, toInt(document.getElementById("refresh-interval").value) || 10); + if (!id) return alert("Enter enemy faction ID"); await fetch("/api/start_enemy_status", { method: "POST", @@ -149,10 +176,100 @@ async function startEnemyStatus() { body: JSON.stringify({ faction_id: id, interval }) }); - setInterval(() => refreshStatus("enemy"), interval * 1000); + if (enemyStatusIntervalHandle) clearInterval(enemyStatusIntervalHandle); + enemyStatusIntervalHandle = setInterval(() => refreshStatus("enemy"), interval * 1000); + refreshStatus("enemy"); } -document.getElementById("friendly-populate-btn").addEventListener("click", populateFriendly); -document.getElementById("enemy-populate-btn").addEventListener("click", populateEnemy); -document.getElementById("friendly-status-btn").addEventListener("click", startFriendlyStatus); -document.getElementById("enemy-status-btn").addEventListener("click", startEnemyStatus); +// ---- Drag & drop handling for drop-zones ---- +function setupDropZones() { + const dropZones = document.querySelectorAll(".drop-zone, .member-list"); + + dropZones.forEach(zone => { + zone.addEventListener("dragover", (e) => { + e.preventDefault(); + // peek at payload to determine if valid + const raw = e.dataTransfer.types.includes("text/plain") ? e.dataTransfer.getData("text/plain") : null; + let valid = false; + if (raw) { + try { + const p = JSON.parse(raw); + const kind = p.kind; + const zoneType = zone.classList.contains("friendly-zone") || zone.id.includes("friendly") ? "friendly" : + zone.classList.contains("enemy-zone") || zone.id.includes("enemy") ? "enemy" : null; + if (zoneType && kind === zoneType) valid = true; + } catch {} + } + zone.classList.toggle("dragover-valid", valid); + zone.classList.toggle("dragover-invalid", !valid); + }); + + zone.addEventListener("dragleave", () => { + zone.classList.remove("dragover-valid", "dragover-invalid"); + }); + + zone.addEventListener("drop", (e) => { + e.preventDefault(); + zone.classList.remove("dragover-valid", "dragover-invalid"); + const raw = e.dataTransfer.getData("text/plain"); + if (!raw) return; + let payload; + try { payload = JSON.parse(raw); } catch { return; } + const { kind, id } = payload; + const idn = parseInt(id); + if (!kind || !idn) return; + + // Determine target zone type (friendly or enemy) for validation + const zoneType = zone.classList.contains("friendly-zone") || zone.id.includes("friendly") ? "friendly" : + zone.classList.contains("enemy-zone") || zone.id.includes("enemy") ? "enemy" : null; + if (zoneType !== kind) { + // invalid drop + return; + } + + // Get member object from maps + const map = kind === "friendly" ? friendlyMembers : enemyMembers; + const member = map.get(idn); + if (!member) return; + + // Remove from original parent if exists (so it moves) + const prev = member.domElement?.parentElement; + if (prev && prev !== zone) prev.removeChild(member.domElement); + + // Append to target zone + zone.appendChild(member.domElement); + }); + }); +} + +// ---- Allow clicking a card to log for debug (optional) ---- +function attachCardClickLogging() { + document.addEventListener("click", (e) => { + const el = e.target.closest(".member-card"); + if (!el) return; + const id = parseInt(el.dataset.id); + const kind = el.dataset.kind; + const map = kind === "friendly" ? friendlyMembers : enemyMembers; + const member = map.get(id); + if (member) console.log("Member clicked:", member); + }); +} + +// ---- Initialize UI wiring ---- +function wireUp() { + document.getElementById("friendly-populate-btn").addEventListener("click", populateFriendly); + document.getElementById("enemy-populate-btn").addEventListener("click", populateEnemy); + document.getElementById("friendly-status-btn").addEventListener("click", startFriendlyStatus); + document.getElementById("enemy-status-btn").addEventListener("click", startEnemyStatus); + + setupDropZones(); + attachCardClickLogging(); +} + +// ---- On load: wire and attempt to load any existing JSON lists ---- +document.addEventListener("DOMContentLoaded", async () => { + wireUp(); + // attempt to load existing data (if files exist) + await loadMembers("friendly"); + await loadMembers("enemy"); +}); diff --git a/static/styles.css b/static/styles.css index 6c299d7..6237fc8 100644 --- a/static/styles.css +++ b/static/styles.css @@ -1,3 +1,4 @@ +/* --- base --- */ body { font-family: Arial, sans-serif; background-color: #1e1e2f; @@ -8,165 +9,195 @@ body { } .container { - width: 95%; - max-width: 1300px; - margin: 2rem auto; + width: 96%; + max-width: 1400px; + margin: 1.5rem auto; } +/* top bar */ .top-bar { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 2rem; + margin-bottom: 1rem; } .interval-box { background-color: #3a3a4d; - padding: 0.75rem 1rem; - border-radius: 10px; + padding: 0.5rem 0.8rem; + border-radius: 8px; display: flex; - flex-direction: column; - gap: 0.4rem; + gap: 0.5rem; + align-items: center; } +.interval-box label { color: #ffcc66; font-size: 0.9rem; margin-right: 8px; } +.interval-box input { width: 80px; padding: 6px; border-radius: 6px; border: none; } -.interval-box label { - font-size: 0.9rem; - color: #ffcc66; -} - -.interval-box input { - padding: 0.5rem; - border-radius: 6px; - border: none; -} - -.faction-row { +/* main split */ +.main-row { display: flex; - flex-direction: row !important; - justify-content: space-between; - gap: 2rem; + gap: 1.5rem; } -.faction-card { - flex: 1; - background-color: #2c2c3e; - padding: 1.5rem; - border-radius: 12px; - box-shadow: 0 0 20px rgba(0,0,0,0.5); +/* left column: stacked friendly / enemy */ +.left-col { + width: 48%; display: flex; flex-direction: column; gap: 1rem; - width: 100%; - max-width: 600px; } -.faction-card h2 { - color: #66ccff; +/* right column: groups grid */ +.right-col { + width: 52%; } -input[type="number"] { +.groups-grid { + display: flex; + flex-direction: column; + gap: 1rem; +} + +/* group card */ +.group { + background: #232331; + border-radius: 10px; + padding: 0.6rem; + min-height: 140px; + display: flex; + flex-direction: column; + gap: 0.5rem; + border: 1px solid rgba(255,255,255,0.03); +} + +.group-title { + font-weight: bold; + color: #ffcc66; + margin-bottom: 4px; +} + +/* zones layout inside group */ +.group-zones { + display: flex; + gap: 0.5rem; + height: 100%; +} + +/* drop zones */ +.drop-zone { + flex: 1; + background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.02)); + border-radius: 8px; + border: 2px dashed rgba(255,255,255,0.03); + padding: 8px; + display: flex; + flex-direction: column; + gap: 6px; + align-items: stretch; + overflow-y: auto; + min-width: 0; +} + +/* subtle label */ +.zone-label { + font-size: 0.9rem; + color: #99a7bf; + margin-bottom: 4px; +} + +/* highlight when a valid draggable is over */ +.drop-zone.dragover-valid { + background: rgba(51,153,255,0.06); + border-color: rgba(102,204,255,0.4); +} + +/* invalid dragover */ +.drop-zone.dragover-invalid { + background: rgba(255,77,77,0.06); + border-color: rgba(255,77,77,0.4); +} + +/* friendly/enemy specific coloring for zone headers */ +.friendly-zone .zone-label { color: #8fd38f; } +.enemy-zone .zone-label { color: #ff9b9b; } + +/* Faction card containers on left */ +.faction-card.small { + background-color: #2c2c3e; + padding: 1rem; + border-radius: 10px; + box-shadow: 0 0 18px rgba(0,0,0,0.45); + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.faction-card.small h2 { color: #66ccff; margin: 0; } +.faction-card .controls { display:flex; gap: 0.5rem; align-items:center; margin-bottom: 6px; } +.faction-card .controls input { padding: 0.5rem; border-radius:6px; border: none; } + +/* member list in left column */ +.member-list { + max-height: 380px; + overflow-y: auto; + background: #1a1a26; + padding: 0.6rem; + border-radius: 8px; + border: 1px solid rgba(255,255,255,0.02); + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +/* member card (used both in lists and zones) */ +.member-card { + background-color: #3a3a4d; + color: #f0f0f0; padding: 0.7rem; - border-radius: 6px; - border: none; + border-radius: 8px; + display: flex; + flex-direction: row; + align-items: center; + gap: 0.8rem; + box-shadow: 0 0 10px rgba(0,0,0,0.45); + cursor: grab; } +/* name and stat blocks */ +.member-card .name { min-width: 110px; color: #66ccff; font-weight: bold; } +.member-card .stats { color: #f0f0f0; font-size: 0.9rem; line-height: 1.2; } + +/* Friendly name color */ +.member-card.friendly .name { + color: #4cff4c; /* Green */ +} + +/* Enemy name color */ +.member-card.enemy .name { + color: #ff4c4c; /* Red */ +} + + +/* small status span; color is applied by classes */ +.status-text { font-weight: 700; padding-left: 6px; } + +/* status color classes */ +.status-ok { color: #28a745; text-shadow: 0 0 2px rgba(40,167,69,0.25); } +.status-traveling { color: #3399ff; text-shadow: 0 0 2px rgba(51,153,255,0.25); } +.status-hospitalized { color: #ff4d4d; text-shadow: 0 0 2px rgba(255,77,77,0.25); } + +/* buttons */ button { - padding: 0.7rem 1rem; + padding: 0.5rem 0.7rem; border-radius: 6px; border: none; background-color: #66ccff; color: #1e1e2f; font-weight: bold; cursor: pointer; - transition: background-color 0.2s; } +button:hover { background-color: #3399ff; } -button:hover { - background-color: #3399ff; -} - -.member-list { - margin-top: 1rem; - max-height: 350px; - overflow-y: auto; - background: #1a1a26; - padding: 0.8rem; - border-radius: 10px; -} - -.member-card { - background-color: #3a3a4d; - padding: 1rem; - margin: 0.5rem 0; - border-radius: 10px; - display: flex; - flex-direction: row; /* horizontal layout */ - align-items: center; - justify-content: flex-start; - gap: 2rem; - box-shadow: 0 0 15px rgba(0, 0, 0, 0.5); - cursor: grab; - min-width: 200px; -} - -.member-card:active { - cursor: grabbing; -} - -/* Name section */ -.member-card .name { - font-weight: bold; - color: #66ccff; - min-width: 120px; /* ensures spacing */ -} - -/* Stats section */ -.member-card .stats { - font-size: 0.9rem; - line-height: 1.3; - color: #f0f0f0; -} - -.member-card strong { - font-size: 1rem; - min-width: 100px; -} - -.member-card span { - font-size: 0.9rem; -} - -.member-card:hover { - background-color: #4a4a60; -} - -#friendly-container, -#enemy-container { - max-height: 400px; - overflow-y: auto; - padding: 0.5rem; - border: 1px solid #444; - border-radius: 10px; - background-color: #2c2c3e; -} - -#friendly-container::-webkit-scrollbar, -#enemy-container::-webkit-scrollbar { - width: 8px; -} - -#friendly-container::-webkit-scrollbar-thumb, -#enemy-container::-webkit-scrollbar-thumb { - background-color: #66ccff; - border-radius: 4px; -} - -#friendly-container::-webkit-scrollbar-track, -#enemy-container::-webkit-scrollbar-track { - background-color: #2c2c3e; - border-radius: 4px; -} - -.status-ok { color: #28a745; font-weight: bold; } -.status-traveling { color: #3399ff; font-weight: bold; } -.status-hospitalized { color: #ff4d4d; font-weight: bold; } +/* scrollbar niceties for drop zones and lists */ +.member-list::-webkit-scrollbar, .drop-zone::-webkit-scrollbar { width: 8px; height: 8px; } +.member-list::-webkit-scrollbar-thumb, .drop-zone::-webkit-scrollbar-thumb { background: #66ccff; border-radius: 4px; } diff --git a/templates/dashboard.html b/templates/dashboard.html index b3009b1..ef372f3 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -1,42 +1,111 @@ - + War Dashboard - +
-

War Dashboard

- +
- -
- -
-

Friendly Faction

- - - -
+
+ +
+
+

Friendly Faction

+
+ + + +
+
+
+ +
+

Enemy Faction

+
+ + + +
+
+
- -
-

Enemy Faction

- - - -
-
-
-
+ +
+
+ +
+
Group 1
+
+
+
Friendly
+
+
+
Enemy
+
+
+
+ +
+
Group 2
+
+
+
Friendly
+
+
+
Enemy
+
+
+
+ +
+
Group 3
+
+
+
Friendly
+
+
+
Enemy
+
+
+
+ +
+
Group 4
+
+
+
Friendly
+
+
+
Enemy
+
+
+
+ +
+
Group 5
+
+
+
Friendly
+
+
+
Enemy
+
+
+
+
+
+
+