diff --git a/.gitignore b/.gitignore
index cb2dfcd..9b822a3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,4 @@ App_Data/
# OS cruft
Thumbs.db
Desktop.ini
+Properties/launchSettings.json
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..5a938ce
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,4 @@
+{
+ "tabWidth": 4,
+ "useTabs": false
+}
diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json
index 16000a6..e719358 100644
--- a/Properties/launchSettings.json
+++ b/Properties/launchSettings.json
@@ -16,7 +16,7 @@
"applicationUrl": "http://localhost:5116",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
- "ADMIN_PASSWORD": "changeme"
+ "ADMIN_PASSWORD": "cookiedonut"
}
},
"https": {
@@ -26,7 +26,7 @@
"applicationUrl": "https://localhost:7103;http://localhost:5116",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
- "ADMIN_PASSWORD": "changeme"
+ "ADMIN_PASSWORD": "cookiedonut"
}
},
"IIS Express": {
diff --git a/wwwroot/app.js b/wwwroot/app.js
index e237c59..290da77 100644
--- a/wwwroot/app.js
+++ b/wwwroot/app.js
@@ -1,272 +1,324 @@
import { api, adminApi } from "./js/api.js";
-import { t, setLanguage, getLanguage, initI18n, onLanguageChange } from "./js/i18n.js";
+import {
+ t,
+ setLanguage,
+ getLanguage,
+ initI18n,
+ onLanguageChange,
+} from "./js/i18n.js";
initI18n();
const state = {
- isAuthenticated: false,
- authMode: "login",
- me: null,
- phase: null,
- prevPhase: null,
- counts: null,
- mySuggestions: [],
- allSuggestions: [],
- allSuggestionsSig: null,
- myVotes: [],
- results: [],
- votesRendered: false
+ isAuthenticated: false,
+ authMode: "login",
+ me: null,
+ phase: null,
+ prevPhase: null,
+ counts: null,
+ mySuggestions: [],
+ allSuggestions: [],
+ allSuggestionsSig: null,
+ myVotes: [],
+ results: [],
+ votesRendered: false,
};
const $ = (id) => document.getElementById(id);
const toastEl = $("toast");
function toast(msg, isError = false) {
- if (!toastEl) return;
- toastEl.textContent = msg;
- toastEl.classList.remove("hidden");
- toastEl.classList.toggle("error", isError);
- setTimeout(() => toastEl.classList.add("hidden"), 2000);
+ if (!toastEl) return;
+ toastEl.textContent = msg;
+ toastEl.classList.remove("hidden");
+ toastEl.classList.toggle("error", isError);
+ setTimeout(() => toastEl.classList.add("hidden"), 2000);
}
const getSavedUsername = () => localStorage.getItem("last_username") || "";
const setSavedUsername = (name) => localStorage.setItem("last_username", name);
function setAuthUI(isAuthed) {
- const main = document.querySelector("main");
- const statusBar = document.querySelector(".status-bar");
- const authCard = $("auth-card");
- [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 || !state.me?.isAdmin);
- if (!isAuthed) {
- const adminCard = $("admin-card");
- if (adminCard) adminCard.classList.add("hidden");
- const loginUser = $("login-username");
- const cachedUser = getSavedUsername();
- if (loginUser && cachedUser && !loginUser.dataset.userEditing && !loginUser.value) {
- loginUser.value = cachedUser;
+ const main = document.querySelector("main");
+ const statusBar = document.querySelector(".status-bar");
+ const authCard = $("auth-card");
+ [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 || !state.me?.isAdmin);
+ if (!isAuthed) {
+ const adminCard = $("admin-card");
+ if (adminCard) adminCard.classList.add("hidden");
+ const loginUser = $("login-username");
+ const cachedUser = getSavedUsername();
+ if (
+ loginUser &&
+ cachedUser &&
+ !loginUser.dataset.userEditing &&
+ !loginUser.value
+ ) {
+ loginUser.value = cachedUser;
+ }
}
- }
}
function setAuthMode(mode) {
- state.authMode = mode;
- document.querySelectorAll(".auth-form").forEach(form => {
- form.classList.toggle("hidden", form.dataset.mode !== mode);
- });
- const title = $("auth-title");
- const toggleBtn = $("auth-toggle");
- if (title) {
- title.textContent = mode === "login" ? t("auth.loginHeading") : t("auth.registerHeading");
- }
- if (toggleBtn) {
- toggleBtn.textContent = mode === "login" ? t("auth.switchToRegister") : t("auth.switchToLogin");
- }
+ state.authMode = mode;
+ document.querySelectorAll(".auth-form").forEach((form) => {
+ form.classList.toggle("hidden", form.dataset.mode !== mode);
+ });
+ const title = $("auth-title");
+ const toggleBtn = $("auth-toggle");
+ if (title) {
+ title.textContent =
+ mode === "login"
+ ? t("auth.loginHeading")
+ : t("auth.registerHeading");
+ }
+ if (toggleBtn) {
+ toggleBtn.textContent =
+ mode === "login"
+ ? t("auth.switchToRegister")
+ : t("auth.switchToLogin");
+ }
}
function clearUserState() {
- state.me = null;
- state.phase = null;
- state.prevPhase = null;
- state.counts = null;
- state.mySuggestions = [];
- state.allSuggestions = [];
- state.myVotes = [];
- state.results = [];
- state.votesRendered = false;
- const adminCard = $("admin-card");
- if (adminCard) adminCard.classList.add("hidden");
+ state.me = null;
+ state.phase = null;
+ state.prevPhase = null;
+ state.counts = null;
+ state.mySuggestions = [];
+ state.allSuggestions = [];
+ state.myVotes = [];
+ state.results = [];
+ state.votesRendered = false;
+ const adminCard = $("admin-card");
+ if (adminCard) adminCard.classList.add("hidden");
}
function handleAuthError(err) {
- if (err?.status === 401) {
- clearUserState();
- state.isAuthenticated = false;
- setAuthUI(false);
- return true;
- }
- toast(err?.message || t("toast.unexpected"), true);
- return false;
+ if (err?.status === 401) {
+ clearUserState();
+ state.isAuthenticated = false;
+ setAuthUI(false);
+ return true;
+ }
+ toast(err?.message || t("toast.unexpected"), true);
+ return false;
}
async function loadState() {
- const [me, stateData] = await Promise.all([api.me(), api.state()]);
- state.isAuthenticated = true;
- state.me = me;
- state.prevPhase = state.phase;
- state.phase = stateData.currentPhase;
- state.counts = stateData;
- if (state.prevPhase !== state.phase && state.phase === "Vote") {
- state.votesRendered = false;
- }
- setAuthUI(true);
- renderWelcome();
- renderPhasePill();
- renderCounts();
+ const [me, stateData] = await Promise.all([api.me(), api.state()]);
+ state.isAuthenticated = true;
+ state.me = me;
+ state.prevPhase = state.phase;
+ state.phase = stateData.currentPhase;
+ state.counts = stateData;
+ if (state.prevPhase !== state.phase && state.phase === "Vote") {
+ state.votesRendered = false;
+ }
+ setAuthUI(true);
+ renderWelcome();
+ renderPhasePill();
+ renderCounts();
}
async function loadSuggestData() {
- if (state.phase !== "Suggest") return;
- state.mySuggestions = await api.mySuggestions();
- renderMySuggestions();
+ if (state.phase !== "Suggest") return;
+ state.mySuggestions = await api.mySuggestions();
+ renderMySuggestions();
}
async function loadRevealData() {
- if (state.phase === "Reveal" || state.phase === "Vote" || state.phase === "Results") {
- const latest = await api.allSuggestions();
- const latestSig = signatureSuggestions(latest);
- const changed = latestSig !== state.allSuggestionsSig;
- state.allSuggestions = latest;
- state.allSuggestionsSig = latestSig;
- renderAllSuggestions();
- renderPhaseTitles();
- if (state.phase === "Vote" && changed) {
- state.votesRendered = false;
+ if (
+ state.phase === "Reveal" ||
+ state.phase === "Vote" ||
+ state.phase === "Results"
+ ) {
+ const latest = await api.allSuggestions();
+ const latestSig = signatureSuggestions(latest);
+ const changed = latestSig !== state.allSuggestionsSig;
+ state.allSuggestions = latest;
+ state.allSuggestionsSig = latestSig;
+ renderAllSuggestions();
+ renderPhaseTitles();
+ if (state.phase === "Vote" && changed) {
+ state.votesRendered = false;
+ }
}
- }
}
async function loadVoteData() {
- if (state.phase !== "Vote") return;
- const votes = await api.myVotes();
- state.myVotes = votes;
- if (!state.votesRendered) {
- renderVotes();
- state.votesRendered = true;
- } else {
- syncVoteScores();
- }
+ if (state.phase !== "Vote") return;
+ const votes = await api.myVotes();
+ state.myVotes = votes;
+ if (!state.votesRendered) {
+ renderVotes();
+ state.votesRendered = true;
+ } else {
+ syncVoteScores();
+ }
}
async function loadResults() {
- if (state.phase !== "Results") return;
- state.results = await api.results();
- renderResults();
+ if (state.phase !== "Results") return;
+ state.results = await api.results();
+ renderResults();
}
function renderPhasePill() {
- const phaseKey = typeof state.phase === "string" ? state.phase.toLowerCase() : null;
- $("phase-pill").textContent = phaseKey ? "" : t("phase.loading");
- document.querySelectorAll(".phase-view").forEach((el) => el.classList.add("hidden"));
- const viewMap = {
- Suggest: "suggest-view",
- Reveal: "reveal-view",
- Vote: "vote-view",
- Results: "results-view"
- };
- const id = viewMap[state.phase];
- if (id) $(id).classList.remove("hidden");
- const phaseSelect = $("phase-select");
- if (phaseSelect && !phaseSelect.dataset.userEditing) {
- phaseSelect.value = state.phase || "Suggest";
- }
+ const phaseKey =
+ typeof state.phase === "string" ? state.phase.toLowerCase() : null;
+ $("phase-pill").textContent = phaseKey ? "" : t("phase.loading");
+ document
+ .querySelectorAll(".phase-view")
+ .forEach((el) => el.classList.add("hidden"));
+ const viewMap = {
+ Suggest: "suggest-view",
+ Reveal: "reveal-view",
+ Vote: "vote-view",
+ Results: "results-view",
+ };
+ const id = viewMap[state.phase];
+ if (id) $(id).classList.remove("hidden");
+ const phaseSelect = $("phase-select");
+ if (phaseSelect && !phaseSelect.dataset.userEditing) {
+ phaseSelect.value = state.phase || "Suggest";
+ }
}
function renderCounts() {
- if (!state.counts) return;
- $("counts").textContent = t("counts.format", {
- players: state.counts.players,
- suggestions: state.counts.suggestions,
- votes: state.counts.votes
- });
+ if (!state.counts) return;
+ $("counts").textContent = t("counts.format", {
+ 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 || t("auth.defaultName");
- el.textContent = t("auth.welcome", { name });
+ const el = $("welcome-text");
+ if (!el) return;
+ const name =
+ state.me?.displayName?.trim() ||
+ state.me?.username ||
+ t("auth.defaultName");
+ el.textContent = t("auth.welcome", { name });
}
function renderMySuggestions() {
- const wrap = $("my-suggestions");
- if (!wrap) return;
- wrap.innerHTML = "";
- const allowEdit = state.phase === "Suggest" || state.me?.isAdmin;
- state.mySuggestions.forEach((s) => wrap.appendChild(buildCard(s, { showAuthor: false, allowDelete: true, allowEdit })));
+ const wrap = $("my-suggestions");
+ if (!wrap) return;
+ wrap.innerHTML = "";
+ const allowEdit = state.phase === "Suggest" || state.me?.isAdmin;
+ state.mySuggestions.forEach((s) =>
+ wrap.appendChild(
+ buildCard(s, { showAuthor: false, allowDelete: true, allowEdit }),
+ ),
+ );
}
function renderAllSuggestions() {
- const list = $("all-suggestions");
- if (!list) return;
- list.innerHTML = "";
- const allowEdit = !!state.me?.isAdmin;
- const allowDelete = !!state.me?.isAdmin && (state.phase === "Reveal" || state.phase === "Suggest");
- state.allSuggestions.forEach((s) => list.appendChild(buildCard(s, { showAuthor: true, allowEdit, allowDelete })));
- renderPhaseTitles();
+ const list = $("all-suggestions");
+ if (!list) return;
+ list.innerHTML = "";
+ const allowEdit = !!state.me?.isAdmin;
+ const allowDelete =
+ !!state.me?.isAdmin &&
+ (state.phase === "Reveal" || state.phase === "Suggest");
+ state.allSuggestions.forEach((s) =>
+ list.appendChild(
+ buildCard(s, { showAuthor: true, allowEdit, allowDelete }),
+ ),
+ );
+ renderPhaseTitles();
}
function renderVotes() {
- const list = $("vote-list");
- if (!list) return;
- list.innerHTML = "";
- const votesMap = Object.fromEntries(state.myVotes.map((v) => [v.suggestionId, v.score]));
- state.allSuggestions.forEach((s) => {
- const li = buildCard(s, { showAuthor: true, allowEdit: !!state.me?.isAdmin });
- const hasVote = Object.prototype.hasOwnProperty.call(votesMap, s.id);
- const current = hasVote ? votesMap[s.id] : 5; // start neutral when no prior vote
- const displayScore = hasVote ? current : "—";
- const displayEmoji = hasVote ? scoreToEmoji(current) : neutralEmoji();
- const footer = document.createElement("div");
- footer.className = "vote-controls";
- footer.innerHTML = `
+ const list = $("vote-list");
+ if (!list) return;
+ list.innerHTML = "";
+ const votesMap = Object.fromEntries(
+ state.myVotes.map((v) => [v.suggestionId, v.score]),
+ );
+ state.allSuggestions.forEach((s) => {
+ const li = buildCard(s, {
+ showAuthor: true,
+ allowEdit: !!state.me?.isAdmin,
+ });
+ const hasVote = Object.prototype.hasOwnProperty.call(votesMap, s.id);
+ const current = hasVote ? votesMap[s.id] : 5; // start neutral when no prior vote
+ const displayScore = hasVote ? current : "—";
+ const displayEmoji = hasVote ? scoreToEmoji(current) : neutralEmoji();
+ const footer = document.createElement("div");
+ footer.className = "vote-controls";
+ footer.innerHTML = `
${displayScore}
${displayEmoji}`;
- li.querySelector(".card-body").appendChild(footer);
- list.appendChild(li);
- });
- list.querySelectorAll("input[type=range]").forEach((input) => {
- input.addEventListener("input", (e) => {
- const val = Number(e.target.value);
- $("score-" + e.target.dataset.id).textContent = val;
- const emojiEl = $("emoji-" + e.target.dataset.id);
- if (emojiEl) emojiEl.textContent = scoreToEmoji(val);
+ li.querySelector(".card-body").appendChild(footer);
+ list.appendChild(li);
});
- input.addEventListener("change", async (e) => {
- const suggestionId = Number(e.target.dataset.id);
- const score = Number(e.target.value);
- try {
- await api.vote(suggestionId, score);
- toast(t("vote.saved"));
- await loadVoteData();
- } catch (err) {
- toast(err.message, true);
- }
+ list.querySelectorAll("input[type=range]").forEach((input) => {
+ input.addEventListener("input", (e) => {
+ const val = Number(e.target.value);
+ $("score-" + e.target.dataset.id).textContent = val;
+ const emojiEl = $("emoji-" + e.target.dataset.id);
+ if (emojiEl) emojiEl.textContent = scoreToEmoji(val);
+ });
+ input.addEventListener("change", async (e) => {
+ const suggestionId = Number(e.target.dataset.id);
+ const score = Number(e.target.value);
+ try {
+ await api.vote(suggestionId, score);
+ toast(t("vote.saved"));
+ await loadVoteData();
+ } catch (err) {
+ toast(err.message, true);
+ }
+ });
});
- });
}
function syncVoteScores() {
- const votesMap = Object.fromEntries(state.myVotes.map((v) => [v.suggestionId, v.score]));
- Object.entries(votesMap).forEach(([id, score]) => {
- const slider = document.querySelector(`input[type=range][data-id="${id}"]`);
- const scoreLabel = $("score-" + id);
- const emoji = $("emoji-" + id);
- if (slider && score != null) {
- slider.value = score;
- if (scoreLabel) scoreLabel.textContent = score;
- if (emoji) emoji.textContent = scoreToEmoji(score);
- }
- });
- document.querySelectorAll("input[type=range][data-id]").forEach((slider) => {
- const id = slider.dataset.id;
- if (Object.prototype.hasOwnProperty.call(votesMap, Number(id))) return;
- const scoreLabel = $("score-" + id);
- const emoji = $("emoji-" + id);
- if (scoreLabel) scoreLabel.textContent = "—";
- if (emoji) emoji.textContent = neutralEmoji();
- });
+ const votesMap = Object.fromEntries(
+ state.myVotes.map((v) => [v.suggestionId, v.score]),
+ );
+ Object.entries(votesMap).forEach(([id, score]) => {
+ const slider = document.querySelector(
+ `input[type=range][data-id="${id}"]`,
+ );
+ const scoreLabel = $("score-" + id);
+ const emoji = $("emoji-" + id);
+ if (slider && score != null) {
+ slider.value = score;
+ if (scoreLabel) scoreLabel.textContent = score;
+ if (emoji) emoji.textContent = scoreToEmoji(score);
+ }
+ });
+ document
+ .querySelectorAll("input[type=range][data-id]")
+ .forEach((slider) => {
+ const id = slider.dataset.id;
+ if (Object.prototype.hasOwnProperty.call(votesMap, Number(id)))
+ return;
+ const scoreLabel = $("score-" + id);
+ const emoji = $("emoji-" + id);
+ if (scoreLabel) scoreLabel.textContent = "—";
+ if (emoji) emoji.textContent = neutralEmoji();
+ });
}
function renderResults() {
- const container = $("results-list");
- container.innerHTML = "";
- const table = document.createElement("table");
- table.className = "results-table";
- table.innerHTML = `
+ const container = $("results-list");
+ container.innerHTML = "";
+ const table = document.createElement("table");
+ table.className = "results-table";
+ table.innerHTML = `
${t("results.rank")}
@@ -280,16 +332,16 @@ function renderResults() {
${s.description}
` : ""}${title || ""}