Add multilingual support, competition close/reopen, and backup directory

This commit is contained in:
Jan Meinl
2026-05-17 09:18:34 +02:00
parent bb9f3cd3eb
commit 777f11d93c
18 changed files with 1039 additions and 433 deletions
+68 -7
View File
@@ -56,8 +56,15 @@
const r = state.competition.role;
return r === "system_admin" || r === "chief_scorer";
}
function isSysAdmin() {
return state.competition.role === "system_admin";
}
function isClosed() {
return !!state.competition.closed;
}
function canEditPenalty(p) {
if (isClosed()) return false;
if (isChief()) return true;
if (p.created_by === user.id) return true;
return !!state.competition.allow_any_scorer_edit;
@@ -139,11 +146,11 @@
);
const toolbar = el("div", { class: "toolbar" },
el("button", { class: "primary", onclick: () => openPenaltyModal() }, t("add_penalty")),
el("button", { class: "primary", disabled: isClosed(), onclick: () => { if (!isClosed()) openPenaltyModal(); } }, t("add_penalty")),
search,
applyFilter,
el("div", { class: "spacer" }),
isChief() && el("button", { onclick: openApplyByTaskModal }, t("apply_by_task")),
isChief() && !isClosed() && el("button", { onclick: openApplyByTaskModal }, t("apply_by_task")),
isChief() && el("button", { onclick: async () => {
try {
const blob = await API.exportPenaltiesCSV(competitionId);
@@ -1197,18 +1204,19 @@
// ---- Settings tab ------------------------------------------------------
function renderSettingsTab() {
const name = el("input", { type: "text", value: state.competition.name });
const allow = el("input", { type: "checkbox", checked: !!state.competition.allow_any_scorer_edit });
const readonly = isClosed() && !isSysAdmin();
const name = el("input", { type: "text", value: state.competition.name, disabled: readonly });
const allow = el("input", { type: "checkbox", checked: !!state.competition.allow_any_scorer_edit, disabled: readonly });
const currentRulesLang = state.competition.rules_language || "en";
const langOptions = (state.ruleLanguages && state.ruleLanguages.length)
? [...state.ruleLanguages].sort()
: I18N_AVAILABLE;
const rulesLang = el("select", null,
const rulesLang = el("select", { disabled: readonly },
...langOptions.map((l) => el("option", { value: l, selected: l === currentRulesLang },
I18N_NAMES[l] || l))
);
const msg = el("div", { class: "muted", style: { color: "var(--accent)", display: "none" } });
return el("div", { class: "card" },
const card = el("div", { class: "card" },
el("h2", null, t("settings_tab")),
el("div", { class: "field" }, el("label", null, t("competition_name")), name),
el("div", { class: "field" }, el("label", null, t("rules_language")), rulesLang,
@@ -1216,7 +1224,7 @@
el("label", { class: "row", style: { marginTop: "0.5rem" } }, allow, " " + t("allow_any_scorer_edit")),
msg,
el("div", { class: "row", style: { justifyContent: "flex-end", marginTop: "1rem" } },
el("button", { class: "primary", onclick: async () => {
el("button", { class: "primary", disabled: readonly, onclick: async () => {
try {
await API.updateCompetition(competitionId, {
name: name.value,
@@ -1232,6 +1240,57 @@
} }, t("save_settings")),
),
);
if (isSysAdmin()) {
const adminCard = el("div", { class: "card", style: { marginTop: "1rem" } });
adminCard.appendChild(el("h2", null, t("admin_zone")));
if (isClosed()) {
adminCard.appendChild(el("div", { class: "muted", style: { marginBottom: "0.5rem" } },
t("competition_closed_explain"),
state.competition.closed_at ? " (" + state.competition.closed_at + ")" : "",
));
adminCard.appendChild(el("div", { class: "row", style: { gap: "0.5rem", flexWrap: "wrap" } },
el("button", { class: "primary", onclick: async () => {
if (!confirm(t("confirm_reopen_competition"))) return;
try {
await API.reopenCompetition(competitionId);
state.competition = await API.getCompetition(competitionId);
render();
} catch (e) { alert((e.data && e.data.error) || e.message); }
} }, t("reopen_competition")),
el("button", { class: "danger", onclick: async () => {
if (!confirm(t("confirm_delete_competition"))) return;
try {
await API.deleteCompetition(competitionId);
navigate("competitions");
} catch (e) { alert((e.data && e.data.error) || e.message); }
} }, t("delete_competition")),
));
} else {
adminCard.appendChild(el("div", { class: "muted", style: { marginBottom: "0.5rem" } },
t("close_competition_explain")));
adminCard.appendChild(el("div", { class: "row", style: { gap: "0.5rem", flexWrap: "wrap" } },
el("button", { class: "primary", onclick: async () => {
if (!confirm(t("confirm_close_competition"))) return;
try {
const res = await API.closeCompetition(competitionId);
state.competition = await API.getCompetition(competitionId);
if (res && res.backup) alert(t("backup_written", { file: res.backup }));
render();
} catch (e) { alert((e.data && e.data.error) || e.message); }
} }, t("close_competition")),
el("button", { class: "danger", onclick: async () => {
if (!confirm(t("confirm_delete_competition"))) return;
try {
await API.deleteCompetition(competitionId);
navigate("competitions");
} catch (e) { alert((e.data && e.data.error) || e.message); }
} }, t("delete_competition")),
));
}
return el("div", null, card, adminCard);
}
return card;
}
// ---- Main render -------------------------------------------------------
@@ -1249,6 +1308,8 @@
state.competition.name,
" ",
el("span", { class: "badge accent" }, t(state.competition.role)),
isClosed() ? " " : null,
isClosed() ? el("span", { class: "badge warn" }, t("closed")) : null,
),
el("div", { class: "row" },
el("span", { class: "connection-status " + (state.wsOnline ? "online" : "offline") }),