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.
131 lines
5.2 KiB
JavaScript
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();
|
|
})();
|