Files
PenaltyTracker/web/competitions.js
T
CodingPhoenix ae035c3fd4 Add System Admin area and redesign Competitions page
Move the Users table and add a new rule-text editor into a dedicated
admin.html/admin.js page with its own collapsible sidebar (Users,
Rules categories), reachable only for system admins via a button next
to the brand. The Competitions page no longer renders admin-only
controls inline.

Competitions page: cards are now fully clickable to open the
competition (drop the separate "Open" button), the admin-only delete
action moves into a "..." menu (new generic openInlineMenu/
openAnchoredPopover helpers in common.js), and add an open/closed
filter.

Topbar: language switch and logout move out of the topbar into a
small popup under the profile button, which itself opens the full
settings modal for username/display name/language/password.
2026-09-05 13:33:05 +02:00

131 lines
5.2 KiB
JavaScript

(async function () {
const root = document.getElementById("app");
const user = await bootstrapAuth({ requireAuth: true, forbidIfMustChange: true });
if (!user) return;
const state = { competitions: [], filter: "all" };
async function loadCompetitions() {
state.competitions = await API.listCompetitions();
}
async function openCompetitionModal() {
const backdrop = el("div", { class: "modal-backdrop",
onclick: (e) => { if (e.target === backdrop) backdrop.remove(); } });
const nameInput = el("input", { type: "text" });
const allowInput = el("input", { type: "checkbox" });
let langs = I18N_AVAILABLE;
try {
const fromApi = await API.listRuleLanguages();
if (fromApi && fromApi.length) langs = [...fromApi].sort();
} catch (e) {}
const defaultLang = langs.includes(user.language) ? user.language : "en";
const langInput = el("select", null,
...langs.map((l) => el("option", { value: l, selected: l === defaultLang },
I18N_NAMES[l] || l))
);
backdrop.appendChild(el("div", { class: "modal" },
el("h3", null, t("new_competition")),
el("div", { class: "field" }, el("label", null, t("competition_name")), nameInput),
el("div", { class: "field" }, el("label", null, t("rules_language")), langInput,
el("div", { class: "muted small" }, t("rules_language_hint"))),
el("label", { class: "row" }, allowInput, " " + t("allow_any_scorer_edit")),
el("div", { class: "row", style: { justifyContent: "flex-end", marginTop: "1rem" } },
el("button", { onclick: () => backdrop.remove() }, t("cancel")),
el("button", { class: "primary", onclick: async () => {
if (!nameInput.value.trim()) return;
try {
await API.createCompetition({
name: nameInput.value.trim(),
allow_any_scorer_edit: allowInput.checked,
rules_language: langInput.value,
});
backdrop.remove();
await loadCompetitions();
render();
} catch (e) { alert(e.message); }
} }, t("create")),
),
));
document.body.appendChild(backdrop);
}
function openCardMenu(anchorEl, c) {
openInlineMenu(anchorEl, [
{ label: t("delete"), danger: true, onClick: async () => {
if (!confirm(t("confirm_delete_competition_named", { name: c.name }))) return;
try {
await API.deleteCompetition(c.id);
await loadCompetitions();
render();
} catch (e) { alert((e.data && e.data.error) || e.message); }
} },
]);
}
function matchesFilter(c) {
if (state.filter === "open") return !c.closed;
if (state.filter === "closed") return !!c.closed;
return true;
}
function render() {
clearNode(root);
root.appendChild(renderTopbar(user));
const container = el("div", { class: "container" });
const filterSelect = el("select", { onchange: (e) => { state.filter = e.target.value; render(); } },
el("option", { value: "all", selected: state.filter === "all" }, t("filter_all")),
el("option", { value: "open", selected: state.filter === "open" }, t("open")),
el("option", { value: "closed", selected: state.filter === "closed" }, t("closed")),
);
container.appendChild(el("div", { class: "row", style: { justifyContent: "space-between", marginBottom: "0.75rem" } },
el("h2", { style: { margin: 0 } }, t("competitions")),
el("div", { class: "row" },
filterSelect,
user.is_system_admin && el("button", { class: "primary", onclick: openCompetitionModal }, t("new_competition")),
),
));
const filtered = state.competitions.filter(matchesFilter);
if (filtered.length === 0) {
container.appendChild(el("div", { class: "muted" }, t("no_competitions")));
} else {
const grid = el("div", { class: "grid" });
for (const c of filtered) {
const menuBtn = user.is_system_admin
? el("button", { class: "ghost action-btn", title: t("actions"), onclick: (e) => {
e.stopPropagation();
openCardMenu(e.currentTarget, c);
} }, "⋮")
: null;
grid.appendChild(el("div", {
class: "card clickable",
tabindex: "0",
onclick: () => navigate("competition", { id: c.id }),
onkeydown: (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); navigate("competition", { id: c.id }); } },
},
el("div", { class: "row", style: { justifyContent: "space-between", alignItems: "center", flexWrap: "nowrap" } },
el("h2", { title: c.name, style: { margin: 0, minWidth: 0, flex: "1", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, c.name),
menuBtn && el("div", { style: { flexShrink: "0" } }, menuBtn),
),
el("div", { class: "muted", style: { marginTop: "0.5rem" } },
el("span", { class: "badge accent" }, t(c.role)),
c.closed ? " " : null,
c.closed ? el("span", { class: "badge warn" }, t("closed")) : null,
),
));
}
container.appendChild(grid);
}
root.appendChild(container);
}
await loadCompetitions();
render();
})();