Files
GameList/wwwroot/app.js

240 lines
7.5 KiB
JavaScript

import { api, adminApi } from "./js/api.js";
import { t, setLanguage, getLanguage, initI18n, onLanguageChange } from "./js/i18n.js";
import { state, clearUserState, getSavedUsername, setSavedUsername } from "./js/state.js";
import { $, toast } from "./js/dom.js";
import { setupBackgroundPan, triggerCelebration } from "./js/effects.js";
import {
setAuthUI,
setAuthMode,
handleAuthError,
renderWelcome,
renderPhasePill,
renderCounts,
renderMySuggestions,
renderAllSuggestions,
renderVotes,
syncVoteScores,
renderResults,
renderPhaseTitles,
normalizeSuggestionForm,
} from "./js/ui.js";
import {
loadState,
loadSuggestData,
loadRevealData,
loadVoteData,
loadResults,
refreshPhaseData,
} from "./js/data.js";
initI18n();
setupBackgroundPan({ maxOffsetPx: 30, ease: 0.08, scaleFactor: 1.12 });
function setupHandlers() {
const toggleAuth = $("auth-toggle");
if (toggleAuth) {
toggleAuth.addEventListener("click", (e) => {
e.preventDefault();
setAuthMode(state.authMode === "login" ? "register" : "login");
});
}
setAuthMode(state.authMode);
const loginUser = $("login-username");
if (loginUser) {
const markEditing = () => { loginUser.dataset.userEditing = "1"; };
["focus", "input", "keydown"].forEach((evt) => loginUser.addEventListener(evt, markEditing));
loginUser.addEventListener("blur", () => { delete loginUser.dataset.userEditing; });
}
const langSelects = Array.from(document.querySelectorAll(".lang-select"));
const syncLanguageSelects = () => langSelects.forEach((sel) => (sel.value = getLanguage()));
syncLanguageSelects();
langSelects.forEach((sel) => sel.addEventListener("change", () => setLanguage(sel.value)));
onLanguageChange(() => {
syncLanguageSelects();
renderWelcome();
renderPhasePill();
renderCounts();
renderPhaseTitles();
renderMySuggestions();
renderAllSuggestions();
if (state.phase === "Vote") {
renderVotes();
state.votesRendered = true;
syncVoteScores();
}
if (state.phase === "Results") {
renderResults();
}
});
const loginForm = $("login-form");
if (loginForm) {
loginForm.addEventListener("submit", async (e) => {
e.preventDefault();
const username = $("login-username").value.trim();
const password = $("login-password").value;
if (username.length > 24) return toast("Username must be 24 characters or fewer.", true);
if (!username || !password) return toast(t("auth.needCredentials"), true);
try {
await api.login({ username, password });
setSavedUsername(username);
state.isAuthenticated = true;
setAuthUI(true);
await refreshPhaseData();
toast(t("toast.loggedIn"));
} catch (err) {
if (err?.status === 401) return toast(t("auth.invalidCredentials"), true);
if (handleAuthError(err, clearUserState)) return;
}
});
}
const registerForm = $("register-form");
if (registerForm) {
registerForm.addEventListener("submit", async (e) => {
e.preventDefault();
const username = $("register-username").value.trim();
const password = $("register-password").value;
const displayName = $("register-displayName").value.trim();
const adminKey = $("register-adminkey").value.trim();
if (!displayName) return toast(t("toast.displayNameRequired") || "Display name is required.", true);
if (username.length > 24) return toast("Username must be 24 characters or fewer.", true);
if (displayName.length > 16) return toast("Display name must be 16 characters or fewer.", true);
if (!username || !password) return toast(t("auth.needCredentials"), true);
try {
await api.register({ username, password, displayName, adminKey });
setSavedUsername(username);
state.isAuthenticated = true;
setAuthUI(true);
await refreshPhaseData();
toast(t("toast.registered"));
} catch (err) {
if (handleAuthError(err, clearUserState)) return;
toast(err.message, true);
}
});
}
$("suggest-form").addEventListener("submit", async (e) => {
e.preventDefault();
const form = e.target;
const data = normalizeSuggestionForm(new FormData(form));
if (!data.name) return toast(t("toast.nameRequired"), true);
if (data.screenshotUrl && !isValidImageUrl(data.screenshotUrl)) {
return toast(t("toast.invalidImageUrl"), true);
}
try {
await api.createSuggestion(data);
form.reset();
toast(t("toast.suggestionAdded"));
triggerCelebration(form.querySelector("button[type=submit]"));
await loadSuggestData();
} catch (err) {
toast(err.message, true);
}
});
$("set-phase").addEventListener("click", async () => {
const phase = $("phase-select").value;
try {
await adminApi.setPhase(phase);
toast(t("admin.phaseUpdated"));
state.prevPhase = state.phase;
state.phase = phase;
state.votesRendered = false;
renderPhasePill();
$("phase-select").dataset.userEditing = "";
await refreshPhaseData();
} catch (err) {
toast(err.message, true);
}
});
const phaseSelect = $("phase-select");
["focus", "input", "click"].forEach((evt) => {
phaseSelect.addEventListener(evt, () => {
phaseSelect.dataset.userEditing = "1";
});
});
phaseSelect.addEventListener("blur", () => {
phaseSelect.dataset.userEditing = "";
});
$("reset").addEventListener("click", () => adminAction(adminApi.reset, t("admin.resetDone")));
$("factory-reset").addEventListener("click", () => adminAction(adminApi.factoryReset, t("admin.factoryResetDone")));
const logoutBtn = $("logout");
if (logoutBtn) {
logoutBtn.addEventListener("click", async (e) => {
e.preventDefault();
const lastUser = state.me?.username;
try {
await api.logout();
} catch (err) {
toast(err.message, true);
}
clearUserState();
state.isAuthenticated = false;
setAuthUI(false);
if (lastUser) {
setSavedUsername(lastUser);
const loginUser = $("login-username");
if (loginUser) loginUser.value = lastUser;
const loginPass = $("login-password");
if (loginPass) loginPass.value = "";
}
});
}
const adminToggle = $("admin-toggle");
const adminCard = $("admin-card");
const adminClose = $("admin-close");
if (adminToggle && adminCard && adminClose) {
const togglePanel = (show) => adminCard.classList.toggle("hidden", !show);
adminToggle.addEventListener("click", () => togglePanel(adminCard.classList.contains("hidden")));
adminClose.addEventListener("click", () => togglePanel(false));
}
}
async function adminAction(fn, successMessage) {
try {
await fn();
toast(successMessage);
await refreshPhaseData();
} catch (err) {
toast(err.message, true);
}
}
async function main() {
setupHandlers();
try {
await refreshPhaseData();
} catch (err) {
toast(err.message, true);
}
setInterval(() => {
refreshPhaseData().catch((err) => {
if (!handleAuthError(err, clearUserState)) toast(err.message, true);
});
}, 4000);
}
main();
function isValidImageUrl(url) {
if (!url) return true;
try {
const u = new URL(url);
const allowed = ["http:", "https:"];
if (!allowed.includes(u.protocol)) return false;
const path = u.pathname.toLowerCase();
return [".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif"].some((ext) => path.endsWith(ext));
} catch {
return false;
}
}