Working member cards and assignment groups, some formatting
This commit is contained in:
@@ -10,8 +10,5 @@ Features:
|
|||||||
|
|
||||||
|
|
||||||
ToDo:
|
ToDo:
|
||||||
- move interval button to a neutral spot
|
|
||||||
- sections to list faction memebers
|
|
||||||
- needs to be movable objects
|
|
||||||
- Assignment pools depending on stats
|
- Assignment pools depending on stats
|
||||||
- players will be round-robin queued their targets from here
|
- players will be round-robin queued their targets from here
|
||||||
@@ -1,134 +1,156 @@
|
|||||||
// dashboard.js
|
// 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();
|
const enemyMembers = new Map();
|
||||||
|
|
||||||
|
// ---- DOM containers ----
|
||||||
const friendlyContainer = document.getElementById("friendly-container");
|
const friendlyContainer = document.getElementById("friendly-container");
|
||||||
const enemyContainer = document.getElementById("enemy-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");
|
const card = document.createElement("div");
|
||||||
card.classList.add("member-card");
|
card.classList.add("member-card");
|
||||||
|
card.classList.add(kind);
|
||||||
|
card.setAttribute("draggable", "true");
|
||||||
card.dataset.id = member.id;
|
card.dataset.id = member.id;
|
||||||
|
card.dataset.kind = kind;
|
||||||
|
|
||||||
// Clear innerHTML, create structured divs
|
// structured children
|
||||||
const nameDiv = document.createElement("div");
|
const nameDiv = document.createElement("div");
|
||||||
nameDiv.classList.add("name");
|
nameDiv.className = "name";
|
||||||
nameDiv.textContent = member.name;
|
nameDiv.textContent = member.name;
|
||||||
|
|
||||||
const statsDiv = document.createElement("div");
|
const statsDiv = document.createElement("div");
|
||||||
statsDiv.classList.add("stats");
|
statsDiv.className = "stats";
|
||||||
statsDiv.innerHTML = `
|
statsDiv.innerHTML = `Lv: ${member.level} <br> Est: ${member.estimate} <br> Status: <span class="status-text">${member.status || "Unknown"}</span>`;
|
||||||
Level: ${member.level}<br>
|
|
||||||
Estimate: ${member.estimate}<br>
|
|
||||||
Status: <span class="status-text">${member.status || "Unknown"}</span>
|
|
||||||
`;
|
|
||||||
|
|
||||||
card.appendChild(nameDiv);
|
card.appendChild(nameDiv);
|
||||||
card.appendChild(statsDiv);
|
card.appendChild(statsDiv);
|
||||||
|
|
||||||
// Store reference to DOM element
|
// store back-reference
|
||||||
member.domElement = card;
|
member.domElement = card;
|
||||||
|
|
||||||
// Make card draggable
|
// drag events: encode type and id in dataTransfer
|
||||||
card.draggable = true;
|
card.addEventListener("dragstart", (e) => {
|
||||||
card.addEventListener("dragstart", e => {
|
const payload = JSON.stringify({ kind, id: member.id });
|
||||||
e.dataTransfer.setData("text/plain", 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
|
// initial color
|
||||||
updateStatusColor(member);
|
applyStatusClass(member);
|
||||||
|
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStatusColor(member) {
|
// ---- Apply status CSS class to the .status-text inside a member card ----
|
||||||
const statusSpan = member.domElement.querySelector(".status-text");
|
function applyStatusClass(member) {
|
||||||
statusSpan.classList.remove("status-ok", "status-traveling", "status-hospitalized");
|
const span = member.domElement?.querySelector(".status-text");
|
||||||
|
if (!span) return;
|
||||||
if (!member.status) return;
|
span.classList.remove("status-ok", "status-traveling", "status-hospitalized");
|
||||||
|
const s = String(member.status || "").toLowerCase();
|
||||||
const s = member.status.toLowerCase();
|
if (s === "okay") span.classList.add("status-ok");
|
||||||
if (s === "okay") statusSpan.classList.add("status-ok");
|
else if (s === "traveling" || s === "abroad") span.classList.add("status-traveling");
|
||||||
else if (s === "traveling" || s === "abroad") statusSpan.classList.add("status-traveling");
|
else if (s === "hospitalized" || s === "hospital") span.classList.add("status-hospitalized");
|
||||||
else if (s === "hospitalized") statusSpan.classList.add("status-hospitalized");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Load static members from backend into the list containers ----
|
||||||
async function loadMembers(faction) {
|
async function loadMembers(faction) {
|
||||||
const url = faction === "friendly" ? "/api/friendly_members" : "/api/enemy_members";
|
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 {
|
try {
|
||||||
const response = await fetch(url);
|
const res = await fetch(url, { cache: "no-store" });
|
||||||
const statusData = await response.json();
|
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 => {
|
// clear
|
||||||
const member = map.get(parseInt(id));
|
container.innerHTML = "";
|
||||||
if (!member) return;
|
map.clear();
|
||||||
member.status = statusData[id].status;
|
|
||||||
|
|
||||||
// Update DOM
|
arr.forEach(m => {
|
||||||
const statusSpan = member.domElement.querySelector(".status-text");
|
if (!m.status) m.status = "Unknown";
|
||||||
statusSpan.textContent = member.status;
|
const card = createMemberCard(m, faction);
|
||||||
|
map.set(m.id, m);
|
||||||
// Apply correct color class
|
container.appendChild(card);
|
||||||
updateStatusColor(member);
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to refresh status:", err);
|
console.error("loadMembers error", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function populateFriendly() {
|
// ---- Refresh status map and update DOM (does not touch static info or position) ----
|
||||||
const id = parseInt(document.getElementById("friendly-id").value);
|
async function refreshStatus(faction) {
|
||||||
if (!id) return alert("Enter a valid faction ID!");
|
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", {
|
await fetch("/api/populate_friendly", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ faction_id: id, interval: 0 })
|
body: JSON.stringify({ faction_id: id, interval: 0 })
|
||||||
});
|
});
|
||||||
|
await loadMembers("friendly");
|
||||||
loadMembers("friendly");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function populateEnemy() {
|
async function populateEnemy() {
|
||||||
const id = parseInt(document.getElementById("enemy-id").value);
|
const id = toInt(document.getElementById("enemy-id").value);
|
||||||
if (!id) return alert("Enter a valid faction ID!");
|
if (!id) return alert("Enter enemy faction ID");
|
||||||
|
|
||||||
await fetch("/api/populate_enemy", {
|
await fetch("/api/populate_enemy", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ faction_id: id, interval: 0 })
|
body: JSON.stringify({ faction_id: id, interval: 0 })
|
||||||
});
|
});
|
||||||
|
await loadMembers("enemy");
|
||||||
loadMembers("enemy");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Start status refresh loops on backend and JS-side polling ----
|
||||||
|
let friendlyStatusIntervalHandle = null;
|
||||||
|
let enemyStatusIntervalHandle = null;
|
||||||
|
|
||||||
async function startFriendlyStatus() {
|
async function startFriendlyStatus() {
|
||||||
const id = parseInt(document.getElementById("friendly-id").value);
|
const id = toInt(document.getElementById("friendly-id").value);
|
||||||
const interval = parseInt(document.getElementById("refresh-interval").value) || 10;
|
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", {
|
await fetch("/api/start_friendly_status", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -136,12 +158,17 @@ async function startFriendlyStatus() {
|
|||||||
body: JSON.stringify({ faction_id: id, interval })
|
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() {
|
async function startEnemyStatus() {
|
||||||
const id = parseInt(document.getElementById("enemy-id").value);
|
const id = toInt(document.getElementById("enemy-id").value);
|
||||||
const interval = parseInt(document.getElementById("refresh-interval").value) || 10;
|
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", {
|
await fetch("/api/start_enemy_status", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -149,10 +176,100 @@ async function startEnemyStatus() {
|
|||||||
body: JSON.stringify({ faction_id: id, interval })
|
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);
|
// ---- Drag & drop handling for drop-zones ----
|
||||||
document.getElementById("enemy-populate-btn").addEventListener("click", populateEnemy);
|
function setupDropZones() {
|
||||||
document.getElementById("friendly-status-btn").addEventListener("click", startFriendlyStatus);
|
const dropZones = document.querySelectorAll(".drop-zone, .member-list");
|
||||||
document.getElementById("enemy-status-btn").addEventListener("click", startEnemyStatus);
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* --- base --- */
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
background-color: #1e1e2f;
|
background-color: #1e1e2f;
|
||||||
@@ -8,165 +9,195 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
width: 95%;
|
width: 96%;
|
||||||
max-width: 1300px;
|
max-width: 1400px;
|
||||||
margin: 2rem auto;
|
margin: 1.5rem auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* top bar */
|
||||||
.top-bar {
|
.top-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.interval-box {
|
.interval-box {
|
||||||
background-color: #3a3a4d;
|
background-color: #3a3a4d;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.5rem 0.8rem;
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
gap: 0.5rem;
|
||||||
gap: 0.4rem;
|
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 {
|
/* main split */
|
||||||
font-size: 0.9rem;
|
.main-row {
|
||||||
color: #ffcc66;
|
|
||||||
}
|
|
||||||
|
|
||||||
.interval-box input {
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.faction-row {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row !important;
|
gap: 1.5rem;
|
||||||
justify-content: space-between;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.faction-card {
|
/* left column: stacked friendly / enemy */
|
||||||
flex: 1;
|
.left-col {
|
||||||
background-color: #2c2c3e;
|
width: 48%;
|
||||||
padding: 1.5rem;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 0 20px rgba(0,0,0,0.5);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
width: 100%;
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.faction-card h2 {
|
/* right column: groups grid */
|
||||||
color: #66ccff;
|
.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;
|
padding: 0.7rem;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
border: none;
|
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 {
|
button {
|
||||||
padding: 0.7rem 1rem;
|
padding: 0.5rem 0.7rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: #66ccff;
|
background-color: #66ccff;
|
||||||
color: #1e1e2f;
|
color: #1e1e2f;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
}
|
||||||
|
button:hover { background-color: #3399ff; }
|
||||||
|
|
||||||
button:hover {
|
/* scrollbar niceties for drop zones and lists */
|
||||||
background-color: #3399ff;
|
.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; }
|
||||||
|
|
||||||
.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; }
|
|
||||||
|
|||||||
@@ -1,42 +1,111 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<title>War Dashboard</title>
|
<title>War Dashboard</title>
|
||||||
<link rel="stylesheet" href="/static/styles.css">
|
<link rel="stylesheet" href="/static/styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- Top bar: Title + Refresh Interval -->
|
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
<h1>War Dashboard</h1>
|
<h1>War Dashboard</h1>
|
||||||
<div class="interval-box">
|
<div class="interval-box">
|
||||||
<label for="refresh-interval">Refresh Interval (seconds)</label>
|
<label for="refresh-interval">Refresh Interval (seconds)</label>
|
||||||
<input type="number" id="refresh-interval" value="10" min="1">
|
<input type="number" id="refresh-interval" value="10" min="1" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Faction row -->
|
<div class="main-row">
|
||||||
<div class="faction-row">
|
<!-- LEFT: faction lists stacked vertically -->
|
||||||
<!-- Friendly Faction -->
|
<div class="left-col">
|
||||||
<div class="faction-card">
|
<div class="faction-card small">
|
||||||
<h2>Friendly Faction</h2>
|
<h2>Friendly Faction</h2>
|
||||||
<input type="number" id="friendly-id" placeholder="Faction ID">
|
<div class="controls">
|
||||||
<button id="friendly-populate-btn">Populate Friendly</button>
|
<input type="number" id="friendly-id" placeholder="Faction ID" />
|
||||||
<button id="friendly-status-btn">Start Refresh</button>
|
<button id="friendly-populate-btn">Populate</button>
|
||||||
<div id="friendly-container" class="members-container"></div>
|
<button id="friendly-status-btn">Start Refresh</button>
|
||||||
|
</div>
|
||||||
|
<div id="friendly-container" class="member-list" aria-label="Friendly members"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="faction-card small">
|
||||||
|
<h2>Enemy Faction</h2>
|
||||||
|
<div class="controls">
|
||||||
|
<input type="number" id="enemy-id" placeholder="Faction ID" />
|
||||||
|
<button id="enemy-populate-btn">Populate</button>
|
||||||
|
<button id="enemy-status-btn">Start Refresh</button>
|
||||||
|
</div>
|
||||||
|
<div id="enemy-container" class="member-list" aria-label="Enemy members"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Enemy Faction -->
|
<!-- RIGHT: 5 groups, each with friendly + enemy drop zones -->
|
||||||
<div class="faction-card">
|
<div class="right-col">
|
||||||
<h2>Enemy Faction</h2>
|
<div class="groups-grid">
|
||||||
<input type="number" id="enemy-id" placeholder="Faction ID">
|
<!-- Generate 5 groups -->
|
||||||
<button id="enemy-populate-btn">Populate Enemy</button>
|
<div class="group" id="group-1">
|
||||||
<button id="enemy-status-btn">Start Refresh</button>
|
<div class="group-title">Group 1</div>
|
||||||
<div id="enemy-container" class="members-container"></div>
|
<div class="group-zones">
|
||||||
</div>
|
<div class="drop-zone friendly-zone" data-zone="friendly" data-group="1" id="group-1-friendly">
|
||||||
</div>
|
<div class="zone-label">Friendly</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="drop-zone enemy-zone" data-zone="enemy" data-group="1" id="group-1-enemy">
|
||||||
|
<div class="zone-label">Enemy</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group" id="group-2">
|
||||||
|
<div class="group-title">Group 2</div>
|
||||||
|
<div class="group-zones">
|
||||||
|
<div class="drop-zone friendly-zone" data-zone="friendly" data-group="2" id="group-2-friendly">
|
||||||
|
<div class="zone-label">Friendly</div>
|
||||||
|
</div>
|
||||||
|
<div class="drop-zone enemy-zone" data-zone="enemy" data-group="2" id="group-2-enemy">
|
||||||
|
<div class="zone-label">Enemy</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group" id="group-3">
|
||||||
|
<div class="group-title">Group 3</div>
|
||||||
|
<div class="group-zones">
|
||||||
|
<div class="drop-zone friendly-zone" data-zone="friendly" data-group="3" id="group-3-friendly">
|
||||||
|
<div class="zone-label">Friendly</div>
|
||||||
|
</div>
|
||||||
|
<div class="drop-zone enemy-zone" data-zone="enemy" data-group="3" id="group-3-enemy">
|
||||||
|
<div class="zone-label">Enemy</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group" id="group-4">
|
||||||
|
<div class="group-title">Group 4</div>
|
||||||
|
<div class="group-zones">
|
||||||
|
<div class="drop-zone friendly-zone" data-zone="friendly" data-group="4" id="group-4-friendly">
|
||||||
|
<div class="zone-label">Friendly</div>
|
||||||
|
</div>
|
||||||
|
<div class="drop-zone enemy-zone" data-zone="enemy" data-group="4" id="group-4-enemy">
|
||||||
|
<div class="zone-label">Enemy</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group" id="group-5">
|
||||||
|
<div class="group-title">Group 5</div>
|
||||||
|
<div class="group-zones">
|
||||||
|
<div class="drop-zone friendly-zone" data-zone="friendly" data-group="5" id="group-5-friendly">
|
||||||
|
<div class="zone-label">Friendly</div>
|
||||||
|
</div>
|
||||||
|
<div class="drop-zone enemy-zone" data-zone="enemy" data-group="5" id="group-5-enemy">
|
||||||
|
<div class="zone-label">Enemy</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> <!-- groups-grid -->
|
||||||
|
</div> <!-- right-col -->
|
||||||
|
</div> <!-- main-row -->
|
||||||
|
</div> <!-- container -->
|
||||||
|
|
||||||
<script src="/static/dashboard.js"></script>
|
<script src="/static/dashboard.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user