Split frontend UI into feature modules

This commit is contained in:
2026-02-07 02:07:29 +01:00
parent b16bf8007f
commit 34d274d244
9 changed files with 1153 additions and 1093 deletions

98
wwwroot/js/results-ui.js Normal file
View File

@@ -0,0 +1,98 @@
import { t } from "./i18n.js";
import { state } from "./state.js";
import { $ } from "./dom.js";
import { linkRootId, renderLinkBadge, escapeHtml, safeUrl } from "./ui-utils.js";
import { scoreToEmoji } from "./votes-ui.js";
import { openLightbox } from "./modals-ui.js";
export function renderResults() {
const container = $("results-list");
if (!container) return;
container.innerHTML = "";
const table = document.createElement("table");
table.className = "results-table";
table.innerHTML = `
<thead>
<tr>
<th>${t("results.rank")}</th>
<th>${t("results.game")}</th>
<th>${t("results.author")}</th>
<th>${t("results.average")}</th>
<th>${t("results.votesList")}</th>
<th>${t("results.myVote")}</th>
<th>${t("results.links")}</th>
</tr>
</thead>
<tbody></tbody>
`;
const tbody = table.querySelector("tbody");
const rankByRoot = new Map();
let nextRank = 1;
state.results.forEach((r) => {
const root = linkRootId(r);
let rank = rankByRoot.get(root);
if (!rank) {
rank = nextRank++;
rankByRoot.set(root, 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" : "";
row.className = podiumClass;
const safeName = escapeHtml(r.name);
const safeAuthor = escapeHtml(r.author ?? "—");
const safeShot = safeUrl(r.screenshotUrl);
const safeGameUrl = safeUrl(r.gameUrl);
const safeYoutubeUrl = safeUrl(r.youtubeUrl);
row.innerHTML = `
<td class="rank-cell"><span class="medal">${medal}</span></td>
<td class="game-cell">
${safeShot ? `<img class="thumb clickable-thumb" src="${safeShot}" alt="${safeName}">` : ""}
<div class="game-meta">
<div class="title-line">
<span class="title-text">${safeName}</span>
${renderLinkBadge(r)}
</div>
${buildResultMeta(r)}
</div>
</td>
<td class="author-cell">${safeAuthor || "—"}</td>
<td>${r.average?.toFixed ? r.average.toFixed(1) : r.average}</td>
<td>${formatVotes(r.votes)}</td>
<td>${formatMyVote(r.myVote)}</td>
<td>
${safeGameUrl ? `<a class="link compact" href="${safeGameUrl}" target="_blank" rel="noopener">${t("results.link.site")}</a><br>` : ""}
${safeYoutubeUrl ? `<a class="link compact" href="${safeYoutubeUrl}" target="_blank" rel="noopener">${t("results.link.youtube")}</a>` : ""}
</td>
`;
tbody.appendChild(row);
});
const frame = document.createElement("div");
frame.className = "results-frame";
frame.appendChild(table);
container.appendChild(frame);
container.querySelectorAll(".clickable-thumb").forEach((img) => {
img.addEventListener("click", () => openLightbox(img.src, img.alt));
});
}
function buildResultMeta(r) {
const hasPlayers = r.minPlayers || r.maxPlayers;
const players = hasPlayers
? t("card.players", { min: r.minPlayers ?? "?", max: r.maxPlayers ?? "?" })
: null;
const bits = [r.genre ? escapeHtml(r.genre) : null, players].filter(Boolean);
if (bits.length === 0) return "";
return `<div class="muted small">${bits.join(" • ")}</div>`;
}
function formatVotes(votes) {
if (!Array.isArray(votes) || votes.length === 0) return "⚠️";
const sorted = [...votes].sort((a, b) => a - b);
return sorted.map((v) => scoreToEmoji(v)).join("");
}
function formatMyVote(score) {
if (score == null || Number.isNaN(score)) return "—";
return `${score} ${scoreToEmoji(score)}`;
}