Add admin accounts and streamlined header UI
This commit is contained in:
@@ -30,7 +30,7 @@ function setAuthUI(isAuthed) {
|
||||
[main, statusBar].forEach(el => el?.classList.toggle("hidden", !isAuthed));
|
||||
if (authCard) authCard.classList.toggle("hidden", isAuthed);
|
||||
const adminToggle = $("admin-toggle");
|
||||
if (adminToggle) adminToggle.classList.toggle("hidden", !isAuthed);
|
||||
if (adminToggle) adminToggle.classList.toggle("hidden", !isAuthed || !state.me?.isAdmin);
|
||||
}
|
||||
|
||||
function setAuthMode(mode) {
|
||||
@@ -71,13 +71,9 @@ async function loadState() {
|
||||
state.phase = stateData.currentPhase;
|
||||
state.counts = stateData;
|
||||
setAuthUI(true);
|
||||
renderWelcome();
|
||||
renderPhasePill();
|
||||
renderCounts();
|
||||
const nameInput = $("name-input");
|
||||
if (nameInput && !nameInput.dataset.userEditing) {
|
||||
nameInput.value = me.displayName || "";
|
||||
}
|
||||
applyNameRequirementUI();
|
||||
}
|
||||
|
||||
async function loadSuggestData() {
|
||||
@@ -120,7 +116,6 @@ function renderPhasePill() {
|
||||
if (phaseSelect && !phaseSelect.dataset.userEditing) {
|
||||
phaseSelect.value = state.phase || "Suggest";
|
||||
}
|
||||
applyNameRequirementUI();
|
||||
}
|
||||
|
||||
function renderCounts() {
|
||||
@@ -128,6 +123,13 @@ function renderCounts() {
|
||||
$("counts").textContent = `Players: ${state.counts.players} • Suggestions: ${state.counts.suggestions} • Votes: ${state.counts.votes}`;
|
||||
}
|
||||
|
||||
function renderWelcome() {
|
||||
const el = $("welcome-text");
|
||||
if (!el) return;
|
||||
const name = state.me?.displayName?.trim() || state.me?.username || "Player";
|
||||
el.textContent = `Welcome, ${name}!`;
|
||||
}
|
||||
|
||||
function renderMySuggestions() {
|
||||
const wrap = $("my-suggestions");
|
||||
if (!wrap) return;
|
||||
@@ -258,9 +260,10 @@ function setupHandlers() {
|
||||
const username = $("register-username").value.trim();
|
||||
const password = $("register-password").value;
|
||||
const displayName = $("register-displayName").value.trim();
|
||||
const adminKey = $("register-adminkey").value.trim();
|
||||
if (!username || !password) return toast("Username and password required", true);
|
||||
try {
|
||||
await api.register({ username, password, displayName });
|
||||
await api.register({ username, password, displayName, adminKey });
|
||||
state.isAuthenticated = true;
|
||||
setAuthUI(true);
|
||||
await refreshPhaseData();
|
||||
@@ -272,28 +275,6 @@ function setupHandlers() {
|
||||
});
|
||||
}
|
||||
|
||||
const nameInput = $("name-input");
|
||||
if (nameInput) {
|
||||
["focus", "input"].forEach(evt => {
|
||||
nameInput.addEventListener(evt, () => { nameInput.dataset.userEditing = "1"; });
|
||||
});
|
||||
nameInput.addEventListener("blur", () => { nameInput.dataset.userEditing = ""; });
|
||||
}
|
||||
|
||||
$("save-name").addEventListener("click", async () => {
|
||||
const name = nameInput.value.trim();
|
||||
if (!name) return toast("Name required", true);
|
||||
try {
|
||||
const me = await api.setName(name);
|
||||
state.me = me;
|
||||
nameInput.dataset.userEditing = "";
|
||||
toast("Saved name");
|
||||
applyNameRequirementUI();
|
||||
} catch (err) {
|
||||
toast(err.message, true);
|
||||
}
|
||||
});
|
||||
|
||||
$("suggest-form").addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
@@ -311,9 +292,8 @@ function setupHandlers() {
|
||||
|
||||
$("set-phase").addEventListener("click", async () => {
|
||||
const phase = $("phase-select").value;
|
||||
const adminKey = $("admin-key").value;
|
||||
try {
|
||||
await adminApi.setPhase(phase, adminKey);
|
||||
await adminApi.setPhase(phase);
|
||||
toast("Phase updated");
|
||||
state.phase = phase;
|
||||
$("phase-select").dataset.userEditing = "";
|
||||
@@ -334,7 +314,8 @@ function setupHandlers() {
|
||||
|
||||
const logoutBtn = $("logout");
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener("click", async () => {
|
||||
logoutBtn.addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await api.logout();
|
||||
} catch (err) {
|
||||
@@ -357,9 +338,8 @@ function setupHandlers() {
|
||||
}
|
||||
|
||||
async function adminAction(fn, successMessage) {
|
||||
const adminKey = $("admin-key").value;
|
||||
try {
|
||||
await fn(adminKey);
|
||||
await fn();
|
||||
toast(successMessage);
|
||||
await refreshPhaseData();
|
||||
} catch (err) {
|
||||
@@ -437,32 +417,6 @@ function openLightbox(url, title) {
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
function applyNameRequirementUI() {
|
||||
if (!state.isAuthenticated) return;
|
||||
const requiresName = !state.me?.displayName?.trim();
|
||||
const warning = $("name-warning");
|
||||
if (warning) warning.classList.toggle("hidden", !requiresName);
|
||||
|
||||
const suggestForm = $("suggest-form");
|
||||
if (suggestForm) {
|
||||
suggestForm.querySelectorAll("input,textarea,button").forEach(el => {
|
||||
if (el.id === "save-name") return;
|
||||
el.disabled = requiresName;
|
||||
});
|
||||
suggestForm.classList.toggle("disabled-form", requiresName);
|
||||
}
|
||||
|
||||
const voteList = $("vote-list");
|
||||
if (voteList) {
|
||||
voteList.querySelectorAll("input[type=range]").forEach(el => el.disabled = requiresName);
|
||||
voteList.classList.toggle("disabled-form", requiresName);
|
||||
}
|
||||
|
||||
if (requiresName && state.phase !== "Suggest") {
|
||||
toast("Enter a name to continue.", true);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
setupHandlers();
|
||||
try {
|
||||
|
||||
@@ -22,20 +22,18 @@
|
||||
<form id="register-form" class="stack auth-form hidden" data-mode="register">
|
||||
<input id="register-username" name="username" maxlength="64" placeholder="Username" autocomplete="username" required />
|
||||
<input id="register-password" name="password" type="password" placeholder="Password" autocomplete="new-password" required />
|
||||
<input id="register-displayName" name="displayName" maxlength="64" placeholder="Display name (shows to group)" />
|
||||
<input id="register-displayName" name="displayName" maxlength="64" placeholder="Display name (shows to group)" required />
|
||||
<input id="register-adminkey" name="adminKey" type="password" maxlength="128" placeholder="Admin key (optional)" />
|
||||
<button type="submit">Create account</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="status-bar">
|
||||
<div class="name-bar">
|
||||
<label for="name-input" class="label">Name</label>
|
||||
<input id="name-input" maxlength="64" placeholder="Pick a name" />
|
||||
<button id="save-name" class="ghost">Save</button>
|
||||
<button id="logout" class="ghost">Logout</button>
|
||||
<span class="hint warning hidden" id="name-warning">Name required</span>
|
||||
<div class="status-left">
|
||||
<span id="welcome-text">Welcome!</span>
|
||||
<a id="logout" href="#" class="link inline-link">Logout</a>
|
||||
</div>
|
||||
<div class="phase-bar">
|
||||
<div class="status-right">
|
||||
<span class="status-dot"></span>
|
||||
<span id="phase-pill">Loading…</span>
|
||||
<span class="counts" id="counts">—</span>
|
||||
@@ -89,10 +87,6 @@
|
||||
<h3>Admin</h3>
|
||||
<button id="admin-close" class="ghost">✕</button>
|
||||
</div>
|
||||
<label class="stack">
|
||||
<span class="label">Admin key</span>
|
||||
<input id="admin-key" type="password" placeholder="X-Admin-Key" />
|
||||
</label>
|
||||
<div class="stack horizontal">
|
||||
<select id="phase-select">
|
||||
<option>Suggest</option>
|
||||
|
||||
@@ -8,13 +8,10 @@ const autoBase = (() => {
|
||||
const basePath = metaBase || autoBase;
|
||||
const withBase = (path) => `${basePath}${path}`;
|
||||
|
||||
async function request(path, { method = "GET", body, adminKey } = {}) {
|
||||
async function request(path, { method = "GET", body } = {}) {
|
||||
const res = await fetch(withBase(path), {
|
||||
method,
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...(adminKey ? { "X-Admin-Key": adminKey } : {})
|
||||
},
|
||||
headers: defaultHeaders,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
@@ -34,7 +31,6 @@ async function request(path, { method = "GET", body, adminKey } = {}) {
|
||||
export const api = {
|
||||
state: () => request("/api/state"),
|
||||
me: () => request("/api/me"),
|
||||
setName: (name) => request("/api/me/name", { method: "POST", body: { name } }),
|
||||
register: (payload) => request("/api/auth/register", { method: "POST", body: payload }),
|
||||
login: (payload) => request("/api/auth/login", { method: "POST", body: payload }),
|
||||
logout: () => request("/api/auth/logout", { method: "POST" }),
|
||||
@@ -51,7 +47,7 @@ export const api = {
|
||||
};
|
||||
|
||||
export const adminApi = {
|
||||
setPhase: (phase, adminKey) => request("/api/admin/phase", { method: "POST", body: { phase }, adminKey }),
|
||||
reset: (adminKey) => request("/api/admin/reset", { method: "POST", adminKey }),
|
||||
factoryReset: (adminKey) => request("/api/admin/factory-reset", { method: "POST", adminKey }),
|
||||
setPhase: (phase) => request("/api/admin/phase", { method: "POST", body: { phase } }),
|
||||
reset: () => request("/api/admin/reset", { method: "POST" }),
|
||||
factoryReset: () => request("/api/admin/factory-reset", { method: "POST" }),
|
||||
};
|
||||
|
||||
@@ -10,13 +10,18 @@ body {
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
border: 1px solid #1f2937;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 24px rgba(0,0,0,0.25);
|
||||
padding: 10px 14px;
|
||||
}
|
||||
.status-left, .status-right { display: flex; align-items: center; gap: 10px; }
|
||||
.inline-link { font-size: 14px; }
|
||||
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
@@ -32,20 +37,7 @@ body {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.phase-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.name-bar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
align-items: center;
|
||||
}
|
||||
.name-bar input { width: 180px; }
|
||||
.name-bar .hint { margin: 0; }
|
||||
.phase-bar { display: none; }
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
|
||||
Reference in New Issue
Block a user