152 lines
5.2 KiB
JavaScript
152 lines
5.2 KiB
JavaScript
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);
|
|
const votersTooltip = buildVotersTooltip(r);
|
|
const safeVotersTooltip = escapeHtml(votersTooltip);
|
|
const averageScore =
|
|
r.average?.toFixed && typeof r.average === "number"
|
|
? r.average.toFixed(1)
|
|
: r.average;
|
|
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><span title="${safeVotersTooltip}">${averageScore}</span></td>
|
|
<td>${formatVotes(r.votes, votersTooltip)}</td>
|
|
<td>${formatMyVote(r.myVote, votersTooltip)}</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, tooltip) {
|
|
const safeTooltip = escapeHtml(tooltip);
|
|
if (!Array.isArray(votes) || votes.length === 0) {
|
|
return `<span class="score-emoji" title="${safeTooltip}">⚠️</span>`;
|
|
}
|
|
const sorted = [...votes].sort((a, b) => a - b);
|
|
return sorted
|
|
.map(
|
|
(v) =>
|
|
`<span class="score-emoji" title="${safeTooltip}">${scoreToEmoji(v)}</span>`,
|
|
)
|
|
.join("");
|
|
}
|
|
|
|
function formatMyVote(score, tooltip) {
|
|
if (score == null || Number.isNaN(score)) return "—";
|
|
const safeTooltip = escapeHtml(tooltip);
|
|
return `${score} <span class="score-emoji" title="${safeTooltip}">${scoreToEmoji(score)}</span>`;
|
|
}
|
|
|
|
function buildVotersTooltip(result) {
|
|
const voterNames = Array.isArray(result?.voterNames)
|
|
? result.voterNames
|
|
.filter(
|
|
(name) => typeof name === "string" && name.trim().length > 0,
|
|
)
|
|
.sort((a, b) =>
|
|
a.localeCompare(b, undefined, { sensitivity: "base" }),
|
|
)
|
|
: [];
|
|
if (voterNames.length === 0) return t("results.votersTooltipEmpty");
|
|
return t("results.votersTooltip", { users: voterNames.join(", ") });
|
|
}
|