Add analyzer and frontend lint guardrails
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { api, adminApi } from "./js/api.js";
|
||||
import { t, setLanguage, getLanguage, initI18n, onLanguageChange, faqMarkdown } from "./js/i18n.js";
|
||||
import { state, clearUserState, getSavedUsername, setSavedUsername } from "./js/state.js";
|
||||
import { state, clearUserState, setSavedUsername } from "./js/state.js";
|
||||
import { $, toast } from "./js/dom.js";
|
||||
import {
|
||||
setAuthUI,
|
||||
@@ -22,10 +22,8 @@ import {
|
||||
configureUiRuntime,
|
||||
} from "./js/ui.js";
|
||||
import {
|
||||
loadState,
|
||||
loadSuggestData,
|
||||
loadVoteData,
|
||||
loadResults,
|
||||
refreshPhaseData,
|
||||
} from "./js/data.js";
|
||||
initI18n();
|
||||
|
||||
@@ -7,7 +7,10 @@ function displayPlayerStatus(player) {
|
||||
if (!player) return "";
|
||||
const phase = player.phase;
|
||||
if (phase === "Suggest") return t("admin.statusSuggesting");
|
||||
if (phase === "Vote") return player.finalized ? t("admin.statusFinished") : t("admin.statusVoting");
|
||||
if (phase === "Vote")
|
||||
return player.finalized
|
||||
? t("admin.statusFinished")
|
||||
: t("admin.statusVoting");
|
||||
if (phase === "Results") return t("admin.statusFinished");
|
||||
return phase;
|
||||
}
|
||||
@@ -59,7 +62,9 @@ export function renderAdminLinker() {
|
||||
|
||||
const previousSource = source.value;
|
||||
const previousTarget = target.value;
|
||||
const options = (state.allSuggestions ?? []).slice().sort((a, b) => a.name.localeCompare(b.name));
|
||||
const options = (state.allSuggestions ?? [])
|
||||
.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const fillSelect = (select, placeholderKey) => {
|
||||
select.innerHTML = "";
|
||||
@@ -81,8 +86,10 @@ export function renderAdminLinker() {
|
||||
fillSelect(source, "admin.linkSourcePlaceholder");
|
||||
fillSelect(target, "admin.linkTargetPlaceholder");
|
||||
|
||||
if (previousSource && options.some((s) => String(s.id) === previousSource)) source.value = previousSource;
|
||||
if (previousTarget && options.some((s) => String(s.id) === previousTarget)) target.value = previousTarget;
|
||||
if (previousSource && options.some((s) => String(s.id) === previousSource))
|
||||
source.value = previousSource;
|
||||
if (previousTarget && options.some((s) => String(s.id) === previousTarget))
|
||||
target.value = previousTarget;
|
||||
|
||||
const preventSameSelection = () => {
|
||||
const sourceVal = source.value;
|
||||
|
||||
@@ -24,7 +24,13 @@ export function openLightbox(url, title) {
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
export function openConfirmModal({ title, body, confirmLabel, cancelLabel = t("modal.cancel"), onConfirm }) {
|
||||
export function openConfirmModal({
|
||||
title,
|
||||
body,
|
||||
confirmLabel,
|
||||
cancelLabel = t("modal.cancel"),
|
||||
onConfirm,
|
||||
}) {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "edit-modal";
|
||||
const panel = document.createElement("div");
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { t } from "./i18n.js";
|
||||
import { state } from "./state.js";
|
||||
import { $ } from "./dom.js";
|
||||
import { linkRootId, renderLinkBadge, escapeHtml, safeUrl } from "./ui-utils.js";
|
||||
import {
|
||||
linkRootId,
|
||||
renderLinkBadge,
|
||||
escapeHtml,
|
||||
safeUrl,
|
||||
} from "./ui-utils.js";
|
||||
import { scoreToEmoji } from "./votes-ui.js";
|
||||
import { openLightbox } from "./modals-ui.js";
|
||||
|
||||
@@ -35,9 +40,23 @@ export function renderResults() {
|
||||
rank = nextRank++;
|
||||
rankByRoot.set(root, rank);
|
||||
}
|
||||
const medal = rank === 1 ? "🥇" : rank === 2 ? "🥈" : rank === 3 ? "🥉" : `${rank}`;
|
||||
const medal =
|
||||
rank === 1
|
||||
? "🥇"
|
||||
: rank === 2
|
||||
? "🥈"
|
||||
: rank === 3
|
||||
? "🥉"
|
||||
: `${rank}`;
|
||||
const row = document.createElement("tr");
|
||||
const podiumClass = rank === 1 ? "podium podium-1" : rank === 2 ? "podium podium-2" : rank === 3 ? "podium podium-3" : "";
|
||||
const podiumClass =
|
||||
rank === 1
|
||||
? "podium podium-1"
|
||||
: rank === 2
|
||||
? "podium podium-2"
|
||||
: rank === 3
|
||||
? "podium podium-3"
|
||||
: "";
|
||||
row.className = podiumClass;
|
||||
const safeName = escapeHtml(r.name);
|
||||
const safeAuthor = escapeHtml(r.author ?? "—");
|
||||
@@ -79,9 +98,14 @@ export function renderResults() {
|
||||
function buildResultMeta(r) {
|
||||
const hasPlayers = r.minPlayers || r.maxPlayers;
|
||||
const players = hasPlayers
|
||||
? t("card.players", { min: r.minPlayers ?? "?", max: r.maxPlayers ?? "?" })
|
||||
? t("card.players", {
|
||||
min: r.minPlayers ?? "?",
|
||||
max: r.maxPlayers ?? "?",
|
||||
})
|
||||
: null;
|
||||
const bits = [r.genre ? escapeHtml(r.genre) : null, players].filter(Boolean);
|
||||
const bits = [r.genre ? escapeHtml(r.genre) : null, players].filter(
|
||||
Boolean,
|
||||
);
|
||||
if (bits.length === 0) return "";
|
||||
return `<div class="muted small">${bits.join(" • ")}</div>`;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
escapeHtml,
|
||||
isLinked,
|
||||
linkedPeerTitles,
|
||||
renderLinkBadge,
|
||||
safeUrl,
|
||||
sortByName,
|
||||
} from "./ui-utils.js";
|
||||
@@ -23,7 +22,9 @@ function updateSuggestButtonState() {
|
||||
const count = state.mySuggestions?.length ?? 0;
|
||||
const blocked = count >= limit;
|
||||
btn.disabled = blocked || state.phase !== "Suggest";
|
||||
btn.textContent = blocked ? t("suggest.maxReached") : t("suggest.addButton");
|
||||
btn.textContent = blocked
|
||||
? t("suggest.maxReached")
|
||||
: t("suggest.addButton");
|
||||
}
|
||||
|
||||
export function renderMySuggestions() {
|
||||
@@ -35,7 +36,12 @@ export function renderMySuggestions() {
|
||||
const allowDelete = state.phase === "Suggest" || state.me?.isAdmin;
|
||||
sortByName(state.mySuggestions).forEach((s) =>
|
||||
wrap.appendChild(
|
||||
buildCard(s, { showAuthor: false, allowDelete, allowEdit, lockTitle }),
|
||||
buildCard(s, {
|
||||
showAuthor: false,
|
||||
allowDelete,
|
||||
allowEdit,
|
||||
lockTitle,
|
||||
}),
|
||||
),
|
||||
);
|
||||
updateSuggestButtonState();
|
||||
@@ -69,7 +75,12 @@ export function renderPhaseTitles() {
|
||||
|
||||
export function buildCard(
|
||||
s,
|
||||
{ showAuthor = false, allowDelete = false, allowEdit = false, lockTitle = false },
|
||||
{
|
||||
showAuthor = false,
|
||||
allowDelete = false,
|
||||
allowEdit = false,
|
||||
lockTitle = false,
|
||||
},
|
||||
) {
|
||||
const card = document.createElement("article");
|
||||
card.className = "game-card";
|
||||
@@ -92,9 +103,10 @@ export function buildCard(
|
||||
const linkChip = linked
|
||||
? `<button class="chip icon link-chip${state.me?.isAdmin ? " link-chip-action" : ""}" data-unlink="${s.id}" type="button" title="${linkTooltipSafe}">🔗</button>`
|
||||
: "";
|
||||
const visual = hasImage && safeShot
|
||||
? `<button class="card-visual" data-img="${safeShot}" aria-label="${t("card.openScreenshot")}" style="background-image:url('${cssEscapeUrl(safeShot)}')"></button>`
|
||||
: `<div class="card-visual"></div>`;
|
||||
const visual =
|
||||
hasImage && safeShot
|
||||
? `<button class="card-visual" data-img="${safeShot}" aria-label="${t("card.openScreenshot")}" style="background-image:url('${cssEscapeUrl(safeShot)}')"></button>`
|
||||
: `<div class="card-visual"></div>`;
|
||||
const hasPlayers = s.minPlayers || s.maxPlayers;
|
||||
const players = hasPlayers
|
||||
? `${t("card.players", {
|
||||
@@ -252,9 +264,13 @@ function buildSuggestionForm(initial = {}, lockTitle = false) {
|
||||
return form;
|
||||
|
||||
function initCharCounters(formEl) {
|
||||
const inputs = formEl.querySelectorAll("input[maxlength], textarea[maxlength]");
|
||||
const inputs = formEl.querySelectorAll(
|
||||
"input[maxlength], textarea[maxlength]",
|
||||
);
|
||||
inputs.forEach((input) => {
|
||||
const counter = formEl.querySelector(`.char-counter[data-for="${input.name}"]`);
|
||||
const counter = formEl.querySelector(
|
||||
`.char-counter[data-for="${input.name}"]`,
|
||||
);
|
||||
if (!counter) return;
|
||||
const update = () => {
|
||||
const max = input.maxLength;
|
||||
@@ -268,7 +284,13 @@ function buildSuggestionForm(initial = {}, lockTitle = false) {
|
||||
}
|
||||
}
|
||||
|
||||
function openSuggestionModal({ title, submitLabel, initial = {}, onSubmit, lockTitle = false }) {
|
||||
function openSuggestionModal({
|
||||
title,
|
||||
submitLabel,
|
||||
initial = {},
|
||||
onSubmit,
|
||||
lockTitle = false,
|
||||
}) {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "edit-modal";
|
||||
const panel = document.createElement("div");
|
||||
@@ -323,7 +345,8 @@ function openSuggestionModal({ title, submitLabel, initial = {}, onSubmit, lockT
|
||||
clearError();
|
||||
const min = data.minPlayers;
|
||||
const max = data.maxPlayers;
|
||||
const inRange = (v) => v == null || (Number.isInteger(v) && v >= 1 && v <= 32);
|
||||
const inRange = (v) =>
|
||||
v == null || (Number.isInteger(v) && v >= 1 && v <= 32);
|
||||
const valid =
|
||||
inRange(min) &&
|
||||
inRange(max) &&
|
||||
@@ -450,7 +473,9 @@ function openDeleteConfirmModal(s) {
|
||||
|
||||
function openUnlinkConfirm(s) {
|
||||
const peers = linkedPeerTitles(s);
|
||||
const names = peers.length ? peers.join(", ") : t("admin.unlinkUnknownPeers");
|
||||
const names = peers.length
|
||||
? peers.join(", ")
|
||||
: t("admin.unlinkUnknownPeers");
|
||||
openConfirmModal({
|
||||
title: t("admin.unlinkTitle"),
|
||||
body: t("admin.unlinkBody", { name: s.name, peers: names }),
|
||||
|
||||
@@ -4,7 +4,9 @@ import { state } from "./state.js";
|
||||
export const sortByName = (items) =>
|
||||
(items ?? [])
|
||||
.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" }));
|
||||
.sort((a, b) =>
|
||||
a.name.localeCompare(b.name, undefined, { sensitivity: "base" }),
|
||||
);
|
||||
|
||||
export const truncate = (text, max) => {
|
||||
if (!text) return "";
|
||||
|
||||
@@ -10,9 +10,20 @@ import {
|
||||
openNewSuggestionModal,
|
||||
normalizeSuggestionForm,
|
||||
} from "./suggestions-ui.js";
|
||||
import { renderVotes, scoreToEmoji, syncVoteScores, neutralEmoji, updatePhaseNav } from "./votes-ui.js";
|
||||
import {
|
||||
renderVotes,
|
||||
scoreToEmoji,
|
||||
syncVoteScores,
|
||||
neutralEmoji,
|
||||
updatePhaseNav,
|
||||
} from "./votes-ui.js";
|
||||
import { renderResults } from "./results-ui.js";
|
||||
import { openConfirmModal, openLightbox, openResultsRelockModal, openSuggestionsChangedModal } from "./modals-ui.js";
|
||||
import {
|
||||
openConfirmModal,
|
||||
openLightbox,
|
||||
openResultsRelockModal,
|
||||
openSuggestionsChangedModal,
|
||||
} from "./modals-ui.js";
|
||||
|
||||
export function setAuthUI(isAuthed) {
|
||||
const main = document.querySelector("main");
|
||||
@@ -74,9 +85,9 @@ export function handleAuthError(err, clearUserState) {
|
||||
}
|
||||
|
||||
export function renderPhasePill() {
|
||||
document.querySelectorAll(".phase-view").forEach((el) =>
|
||||
el.classList.add("hidden"),
|
||||
);
|
||||
document
|
||||
.querySelectorAll(".phase-view")
|
||||
.forEach((el) => el.classList.add("hidden"));
|
||||
const viewMap = {
|
||||
Suggest: "suggest-view",
|
||||
Vote: "vote-view",
|
||||
|
||||
@@ -74,7 +74,8 @@ export function renderVotes() {
|
||||
const warn = $("warn-" + suggestionId);
|
||||
const fallbackValue = prevScore ?? 5;
|
||||
const fallbackDisplay = prevScore ?? "—";
|
||||
const fallbackEmoji = prevScore != null ? scoreToEmoji(prevScore) : "⚠️";
|
||||
const fallbackEmoji =
|
||||
prevScore != null ? scoreToEmoji(prevScore) : "⚠️";
|
||||
e.target.value = fallbackValue;
|
||||
if (label) label.textContent = fallbackDisplay;
|
||||
if (emoji) emoji.textContent = fallbackEmoji;
|
||||
@@ -89,7 +90,9 @@ export function renderVotes() {
|
||||
linkedIds.forEach((id) => {
|
||||
const peerWarn = $("warn-" + id);
|
||||
if (peerWarn) peerWarn.classList.add("hidden");
|
||||
const peerSlider = document.querySelector(`input[type=range][data-id="${id}"]`);
|
||||
const peerSlider = document.querySelector(
|
||||
`input[type=range][data-id="${id}"]`,
|
||||
);
|
||||
if (peerSlider) delete peerSlider.dataset.pending;
|
||||
});
|
||||
await getUiRuntime().loadVoteData();
|
||||
@@ -174,7 +177,9 @@ function syncLinkedSliders(sourceEl, value) {
|
||||
if (!linkedAttr) return;
|
||||
const ids = linkedAttr.split(",").filter(Boolean);
|
||||
ids.forEach((id) => {
|
||||
const slider = document.querySelector(`input[type=range][data-id="${id}"]`);
|
||||
const slider = document.querySelector(
|
||||
`input[type=range][data-id="${id}"]`,
|
||||
);
|
||||
if (!slider || slider === sourceEl) return;
|
||||
slider.value = value;
|
||||
const scoreLabel = $("score-" + id);
|
||||
@@ -206,7 +211,9 @@ export function updatePhaseNav() {
|
||||
|
||||
const finalizeBtn = $("finalize-votes");
|
||||
if (finalizeBtn) {
|
||||
finalizeBtn.textContent = state.votesFinal ? t("vote.unfinalize") : t("vote.finalize");
|
||||
finalizeBtn.textContent = state.votesFinal
|
||||
? t("vote.unfinalize")
|
||||
: t("vote.finalize");
|
||||
}
|
||||
|
||||
const voteMissingBadge = $("vote-missing");
|
||||
@@ -226,7 +233,9 @@ export function updatePhaseNav() {
|
||||
|
||||
const voteStatusText = $("vote-status-text");
|
||||
if (voteStatusText) {
|
||||
voteStatusText.textContent = state.votesFinal ? t("nav.voteFinalized") : t("nav.voteHint");
|
||||
voteStatusText.textContent = state.votesFinal
|
||||
? t("nav.voteFinalized")
|
||||
: t("nav.voteHint");
|
||||
}
|
||||
|
||||
renderAdminVoteStatus();
|
||||
@@ -243,7 +252,9 @@ export function updatePhaseNav() {
|
||||
if (voteNext) {
|
||||
const locked = !state.resultsOpen && !isAdmin;
|
||||
voteNext.disabled = locked;
|
||||
voteNext.textContent = locked ? t("nav.waitingForResults") : t("nav.next");
|
||||
voteNext.textContent = locked
|
||||
? t("nav.waitingForResults")
|
||||
: t("nav.next");
|
||||
}
|
||||
|
||||
const adminResultsToggle = $("results-open");
|
||||
|
||||
Reference in New Issue
Block a user