add: elo data model and static rankings page
All checks were successful
Deploy on push / deploy (push) Has been skipped
All checks were successful
Deploy on push / deploy (push) Has been skipped
This commit is contained in:
@@ -1,18 +1,25 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const { getRankedMeals, syncEloWithMeals } = require("./lib/elo");
|
||||
const { loadMeals, repoRoot } = require("./lib/meals");
|
||||
|
||||
const indexTemplatePath = path.join(repoRoot, "templates", "index.html");
|
||||
const indexOutputPath = path.join(repoRoot, "index.html");
|
||||
const rankingsTemplatePath = path.join(repoRoot, "templates", "rankings.html");
|
||||
const rankingsOutputPath = path.join(repoRoot, "rankings.html");
|
||||
const ratingFormatter = new Intl.NumberFormat("en-US", {
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
|
||||
function detectEol(text) {
|
||||
return text.includes("\r\n") ? "\r\n" : "\n";
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return value
|
||||
return String(value)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/</g, "<")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
@@ -37,6 +44,53 @@ function renderGallery(meals, eol) {
|
||||
return meals.map((meal) => renderGalleryItem(meal, eol)).join(eol);
|
||||
}
|
||||
|
||||
function formatRating(rating) {
|
||||
return ratingFormatter.format(rating);
|
||||
}
|
||||
|
||||
function renderRankingSummary(meals, eloData) {
|
||||
const mealLabel = meals.length === 1 ? "meal" : "meals";
|
||||
|
||||
return `\t\t\t\t<p class="ranking-summary">${meals.length} ${mealLabel} seeded at Elo ${formatRating(
|
||||
eloData.defaultRating
|
||||
)} until head-to-head voting starts.</p>`;
|
||||
}
|
||||
|
||||
function renderRankingMeta(rankedMeal) {
|
||||
const ratingText = `Elo ${formatRating(rankedMeal.rating)}`;
|
||||
|
||||
if (rankedMeal.matches === 0) {
|
||||
return `${ratingText} | no votes yet`;
|
||||
}
|
||||
|
||||
const matchLabel = rankedMeal.matches === 1 ? "match" : "matches";
|
||||
return `${ratingText} | ${rankedMeal.wins}-${rankedMeal.losses} record across ${
|
||||
rankedMeal.matches
|
||||
} ${matchLabel}`;
|
||||
}
|
||||
|
||||
function renderRankingItem(rankedMeal, placement, eol) {
|
||||
return [
|
||||
'\t\t\t\t<article class="ranking-card">',
|
||||
`\t\t\t\t\t<p class="ranking-card__placement">#${placement}</p>`,
|
||||
`\t\t\t\t\t<a class="ranking-card__thumbnail" href="images/fulls/${rankedMeal.id}.jpg"><img src="images/thumbs/${rankedMeal.id}.jpg" alt="${escapeHtml(
|
||||
`${rankedMeal.title} thumbnail`
|
||||
)}" /></a>`,
|
||||
'\t\t\t\t\t<div class="ranking-card__body">',
|
||||
`\t\t\t\t\t\t<h2>${escapeHtml(rankedMeal.title)}</h2>`,
|
||||
`\t\t\t\t\t\t<p class="ranking-card__meta">${escapeHtml(renderRankingMeta(rankedMeal))}</p>`,
|
||||
`\t\t\t\t\t\t<p>${escapeHtml(rankedMeal.description)}</p>`,
|
||||
"\t\t\t\t\t</div>",
|
||||
"\t\t\t\t</article>",
|
||||
].join(eol);
|
||||
}
|
||||
|
||||
function renderRankings(rankedMeals, eol) {
|
||||
return rankedMeals
|
||||
.map((rankedMeal, index) => renderRankingItem(rankedMeal, index + 1, eol))
|
||||
.join(eol);
|
||||
}
|
||||
|
||||
function replaceBlock(template, token, replacement) {
|
||||
const pattern = new RegExp(`^[\\t ]*\\{\\{${token}\\}\\}$`, "m");
|
||||
|
||||
@@ -47,20 +101,39 @@ function replaceBlock(template, token, replacement) {
|
||||
return template.replace(pattern, () => replacement);
|
||||
}
|
||||
|
||||
function buildIndex() {
|
||||
function buildIndex(meals = loadMeals()) {
|
||||
const template = fs.readFileSync(indexTemplatePath, "utf8");
|
||||
const eol = detectEol(template);
|
||||
const meals = loadMeals();
|
||||
|
||||
return replaceBlock(template, "gallery_items", renderGallery(meals, eol));
|
||||
}
|
||||
|
||||
function buildRankings(
|
||||
meals = loadMeals(),
|
||||
eloData = syncEloWithMeals(meals)
|
||||
) {
|
||||
const template = fs.readFileSync(rankingsTemplatePath, "utf8");
|
||||
const eol = detectEol(template);
|
||||
const rankedMeals = getRankedMeals(meals, eloData);
|
||||
const withSummary = replaceBlock(
|
||||
template,
|
||||
"ranking_summary",
|
||||
renderRankingSummary(meals, eloData)
|
||||
);
|
||||
|
||||
return replaceBlock(withSummary, "ranking_items", renderRankings(rankedMeals, eol));
|
||||
}
|
||||
|
||||
function writeFile(filePath, contents) {
|
||||
fs.writeFileSync(filePath, contents);
|
||||
}
|
||||
|
||||
function main() {
|
||||
writeFile(indexOutputPath, buildIndex());
|
||||
const meals = loadMeals();
|
||||
const eloData = syncEloWithMeals(meals);
|
||||
|
||||
writeFile(indexOutputPath, buildIndex(meals));
|
||||
writeFile(rankingsOutputPath, buildRankings(meals, eloData));
|
||||
}
|
||||
|
||||
function buildPages() {
|
||||
@@ -74,4 +147,5 @@ if (require.main === module) {
|
||||
module.exports = {
|
||||
buildPages,
|
||||
buildIndex,
|
||||
buildRankings,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user