Rework admin player table and account deletion
This commit is contained in:
@@ -204,17 +204,38 @@ function setupHandlers() {
|
||||
});
|
||||
}
|
||||
|
||||
const grantJokerBtn = $("grant-joker");
|
||||
if (grantJokerBtn) {
|
||||
grantJokerBtn.addEventListener("click", async () => {
|
||||
const playerId = $("joker-player")?.value;
|
||||
if (!playerId) return toast(t("admin.jokerSelectFirst"), true);
|
||||
try {
|
||||
await adminApi.grantJoker(playerId);
|
||||
toast(t("admin.jokerGranted"));
|
||||
await refreshPhaseData();
|
||||
} catch (err) {
|
||||
toast(err.message, true);
|
||||
const playerTable = $("admin-player-table");
|
||||
if (playerTable) {
|
||||
playerTable.addEventListener("click", async (e) => {
|
||||
const grantBtn = e.target.closest("[data-grant-joker]");
|
||||
const deleteBtn = e.target.closest("[data-delete-player]");
|
||||
if (grantBtn) {
|
||||
const playerId = grantBtn.dataset.grantJoker;
|
||||
try {
|
||||
await adminApi.grantJoker(playerId);
|
||||
toast(t("admin.jokerGranted"));
|
||||
await refreshPhaseData();
|
||||
} catch (err) {
|
||||
toast(err.message, true);
|
||||
}
|
||||
} else if (deleteBtn) {
|
||||
const playerId = deleteBtn.dataset.deletePlayer;
|
||||
const name = deleteBtn.dataset.name || "";
|
||||
openConfirmModal({
|
||||
title: t("admin.deleteTitle"),
|
||||
body: t("admin.deleteBody", { name }),
|
||||
confirmLabel: t("admin.deleteConfirm"),
|
||||
onConfirm: async (close) => {
|
||||
try {
|
||||
await adminApi.deletePlayer(playerId);
|
||||
toast(t("admin.deleteDone"));
|
||||
close();
|
||||
await refreshPhaseData();
|
||||
} catch (err) {
|
||||
toast(err.message, true);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -144,22 +144,27 @@
|
||||
<h3 data-i18n="admin.title">Admin</h3>
|
||||
<button id="admin-close" class="ghost">✕</button>
|
||||
</div>
|
||||
<div class="stack" id="admin-vote-status">
|
||||
<div class="stack" id="admin-players">
|
||||
<div class="badge" id="admin-ready-status"></div>
|
||||
<ul class="status-list" id="admin-voter-list"></ul>
|
||||
<div class="table-scroll">
|
||||
<table class="compact" id="admin-player-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="admin.playerName">Name</th>
|
||||
<th data-i18n="admin.playerUsername">Username</th>
|
||||
<th data-i18n="admin.playerStatus">Status</th>
|
||||
<th data-i18n="admin.playerJoker">Joker</th>
|
||||
<th data-i18n="admin.playerDelete">Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<label class="stack toggle-row">
|
||||
<input type="checkbox" id="results-open" />
|
||||
<span data-i18n="admin.resultsOpenToggle">Allow results phase</span>
|
||||
</label>
|
||||
<div class="stack hidden" id="admin-joker">
|
||||
<h4 data-i18n="admin.jokerTitle">Jokers</h4>
|
||||
<label class="stack">
|
||||
<span class="label" data-i18n="admin.jokerSelect">Player</span>
|
||||
<select id="joker-player"></select>
|
||||
</label>
|
||||
<button id="grant-joker" class="secondary" type="button" data-i18n="admin.jokerGive">Grant joker</button>
|
||||
</div>
|
||||
<div class="stack hidden" id="admin-linker">
|
||||
<h4 data-i18n="admin.linkTitle">Link games</h4>
|
||||
<label class="stack">
|
||||
|
||||
@@ -57,6 +57,7 @@ export const adminApi = {
|
||||
reset: () => request("/api/admin/reset", { method: "POST" }),
|
||||
factoryReset: () => request("/api/admin/factory-reset", { method: "POST" }),
|
||||
grantJoker: (playerId) => request("/api/admin/joker", { method: "POST", body: { playerId } }),
|
||||
deletePlayer: (playerId) => request(`/api/admin/players/${playerId}`, { method: "DELETE" }),
|
||||
linkSuggestions: (sourceSuggestionId, targetSuggestionId) =>
|
||||
request("/api/admin/link-suggestions", { method: "POST", body: { sourceSuggestionId, targetSuggestionId } }),
|
||||
unlinkSuggestions: (suggestionId) =>
|
||||
|
||||
@@ -110,12 +110,20 @@ const translations = {
|
||||
"admin.factoryResetDone": "Factory reset complete",
|
||||
"admin.readyForResults": "Ready for results",
|
||||
"admin.waitingForPlayers": "Waiting for players: {names}",
|
||||
"admin.jokerTitle": "Jokers",
|
||||
"admin.jokerSelect": "Player",
|
||||
"admin.jokerGive": "Grant joker",
|
||||
"admin.playerName": "Name",
|
||||
"admin.playerUsername": "Username",
|
||||
"admin.playerStatus": "Status",
|
||||
"admin.playerJoker": "Joker",
|
||||
"admin.playerDelete": "Delete",
|
||||
"admin.grantJokerChip": "Grant",
|
||||
"admin.statusSuggesting": "Suggesting",
|
||||
"admin.statusVoting": "Voting",
|
||||
"admin.statusFinished": "Finished",
|
||||
"admin.deleteTitle": "Delete account?",
|
||||
"admin.deleteBody": "Delete player \"{name}\" and all their games and votes? This cannot be undone.",
|
||||
"admin.deleteConfirm": "Delete",
|
||||
"admin.deleteDone": "Player deleted",
|
||||
"admin.jokerGranted": "Joker granted",
|
||||
"admin.jokerSelectFirst": "Pick a player first.",
|
||||
"admin.jokerPlaceholder": "Pick a player",
|
||||
"admin.linkTitle": "Link games",
|
||||
"admin.linkSource": "Game to link",
|
||||
"admin.linkTarget": "Link to (parent)",
|
||||
@@ -261,12 +269,20 @@ const translations = {
|
||||
"admin.factoryResetDone": "Werkseinstellung abgeschlossen",
|
||||
"admin.readyForResults": "Bereit für Ergebnisse",
|
||||
"admin.waitingForPlayers": "Warten auf: {names}",
|
||||
"admin.jokerTitle": "Joker",
|
||||
"admin.jokerSelect": "Spieler",
|
||||
"admin.jokerGive": "Joker vergeben",
|
||||
"admin.playerName": "Name",
|
||||
"admin.playerUsername": "Benutzername",
|
||||
"admin.playerStatus": "Status",
|
||||
"admin.playerJoker": "Joker",
|
||||
"admin.playerDelete": "Löschen",
|
||||
"admin.grantJokerChip": "Joker",
|
||||
"admin.statusSuggesting": "Vorschlagen",
|
||||
"admin.statusVoting": "Bewerten",
|
||||
"admin.statusFinished": "Fertig",
|
||||
"admin.deleteTitle": "Konto löschen?",
|
||||
"admin.deleteBody": "Spieler \"{name}\" samt Spielen und Stimmen löschen? Dies kann nicht rückgängig gemacht werden.",
|
||||
"admin.deleteConfirm": "Löschen",
|
||||
"admin.deleteDone": "Spieler gelöscht",
|
||||
"admin.jokerGranted": "Joker vergeben",
|
||||
"admin.jokerSelectFirst": "Wähle zuerst einen Spieler.",
|
||||
"admin.jokerPlaceholder": "Spieler wählen",
|
||||
"admin.linkTitle": "Spiele verknüpfen",
|
||||
"admin.linkSource": "Spiel verknüpfen",
|
||||
"admin.linkTarget": "Verknüpfen mit",
|
||||
|
||||
@@ -9,6 +9,10 @@ const sortByName = (items) =>
|
||||
(items ?? [])
|
||||
.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" }));
|
||||
const truncate = (text, max) => {
|
||||
if (!text) return "";
|
||||
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
||||
};
|
||||
|
||||
export function setAuthUI(isAuthed) {
|
||||
const main = document.querySelector("main");
|
||||
@@ -715,20 +719,22 @@ function updateMissingBadgeFromDom() {
|
||||
|
||||
function renderAdminVoteStatus() {
|
||||
if (!state.me?.isAdmin) return;
|
||||
const list = $("admin-voter-list");
|
||||
const status = $("admin-ready-status");
|
||||
const jokerWrap = $("admin-joker");
|
||||
const jokerSelect = $("joker-player");
|
||||
if (!state.adminVoteStatus || !list || !status) return;
|
||||
const statusBadge = $("admin-ready-status");
|
||||
const table = $("admin-player-table")?.querySelector("tbody");
|
||||
if (!state.adminVoteStatus || !statusBadge || !table) return;
|
||||
|
||||
list.innerHTML = "";
|
||||
table.innerHTML = "";
|
||||
state.adminVoteStatus.voters.forEach((v) => {
|
||||
const li = document.createElement("li");
|
||||
const name = v.name?.length > 24 ? `${v.name.slice(0, 21)}…` : v.name;
|
||||
const jokerMark = v.hasJoker ? " 🎟" : "";
|
||||
li.textContent = `${name}${jokerMark} — ${v.finalized ? "✅" : "⏳"}`;
|
||||
li.title = v.name;
|
||||
list.appendChild(li);
|
||||
const tr = document.createElement("tr");
|
||||
const statusText = displayPlayerStatus(v);
|
||||
tr.innerHTML = `
|
||||
<td title="${v.name}">${truncate(v.name, 28)}</td>
|
||||
<td class="muted small" title="${v.username}">${truncate(v.username, 24)}</td>
|
||||
<td>${statusText}</td>
|
||||
<td><button class="chip" data-grant-joker="${v.playerId}" type="button">${v.hasJoker ? "🎟" : t("admin.grantJokerChip")}</button></td>
|
||||
<td><button class="chip danger-chip" data-delete-player="${v.playerId}" data-name="${v.name}" type="button">✕</button></td>
|
||||
`;
|
||||
table.appendChild(tr);
|
||||
});
|
||||
|
||||
const waiting = state.adminVoteStatus.waiting;
|
||||
@@ -736,33 +742,19 @@ function renderAdminVoteStatus() {
|
||||
const waitingDisplay = waiting.map((name) =>
|
||||
name?.length > 24 ? `${name.slice(0, 21)}…` : name,
|
||||
);
|
||||
status.textContent = ready
|
||||
statusBadge.textContent = ready
|
||||
? t("admin.readyForResults")
|
||||
: t("admin.waitingForPlayers", { names: waitingDisplay.join(", ") });
|
||||
status.className = ready ? "badge" : "badge warning";
|
||||
statusBadge.className = ready ? "badge" : "badge warning";
|
||||
}
|
||||
|
||||
if (jokerWrap) jokerWrap.classList.toggle("hidden", state.phase !== "Vote");
|
||||
if (jokerSelect && state.phase === "Vote") {
|
||||
const previous = jokerSelect.value;
|
||||
jokerSelect.innerHTML = "";
|
||||
const placeholder = document.createElement("option");
|
||||
placeholder.value = "";
|
||||
placeholder.disabled = true;
|
||||
placeholder.selected = true;
|
||||
placeholder.textContent = t("admin.jokerPlaceholder");
|
||||
jokerSelect.appendChild(placeholder);
|
||||
|
||||
state.adminVoteStatus.voters.forEach((v) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = v.playerId;
|
||||
opt.textContent = v.hasJoker ? `${v.name} — 🎟` : v.name;
|
||||
jokerSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
if (previous && Array.from(jokerSelect.options).some((o) => o.value === previous)) {
|
||||
jokerSelect.value = previous;
|
||||
}
|
||||
}
|
||||
function displayPlayerStatus(player) {
|
||||
if (!player) return "";
|
||||
const phase = player.phase;
|
||||
if (phase === "Suggest") return t("admin.statusSuggesting");
|
||||
if (phase === "Vote") return player.finalized ? t("admin.statusFinished") : t("admin.statusVoting");
|
||||
if (phase === "Results") return t("admin.statusFinished");
|
||||
return phase;
|
||||
}
|
||||
|
||||
function renderAdminLinker() {
|
||||
|
||||
Reference in New Issue
Block a user