DevPlace CI / test (push) Successful in 32m28s
Add `_alt_from_url` helper in Python rendering and `altFromUrl` in JS ContentRenderer to generate descriptive alt attributes from image filenames, replacing empty alt strings. Update image embed templates and Avatar component to use derived alt text. Migrate inline `style.width` assignments to CSS custom properties (`--poll-pct`, `--hud-xp`, `--quest-pct`, `--bar-pct`, `--quota-pct`) across PollManager, GameFarm, and profile/quest CSS for better maintainability. Introduce new admin, docs, projects, services, and base CSS utility classes. Add "Code Farm" docs page entry.
181 lines
6.1 KiB
JavaScript
181 lines
6.1 KiB
JavaScript
// retoor <retoor@molodetz.nl>
|
||
|
||
import { OptimisticAction } from "./OptimisticAction.js";
|
||
|
||
export class PollManager extends OptimisticAction {
|
||
constructor() {
|
||
super();
|
||
document.addEventListener("click", (event) => this.onClick(event));
|
||
document.addEventListener("submit", (event) => this.onSubmit(event), true);
|
||
}
|
||
|
||
onClick(event) {
|
||
const option = event.target.closest(".poll-option");
|
||
if (option && !option.disabled) {
|
||
event.preventDefault();
|
||
this.vote(option);
|
||
return;
|
||
}
|
||
const toggle = event.target.closest("[data-poll-toggle]");
|
||
if (toggle) {
|
||
event.preventDefault();
|
||
this.toggleBuilder(toggle);
|
||
return;
|
||
}
|
||
const addOption = event.target.closest("[data-poll-add-option]");
|
||
if (addOption) {
|
||
event.preventDefault();
|
||
this.addOption(addOption);
|
||
return;
|
||
}
|
||
const removeOption = event.target.closest("[data-poll-remove-option]");
|
||
if (removeOption) {
|
||
event.preventDefault();
|
||
this.removeOption(removeOption);
|
||
}
|
||
}
|
||
|
||
async vote(option) {
|
||
const poll = option.closest(".poll");
|
||
if (!poll) {
|
||
return;
|
||
}
|
||
const pollUid = poll.dataset.pollUid;
|
||
const optionUid = option.dataset.optionUid;
|
||
await this.submit(`/polls/${pollUid}/vote`, { option_uid: optionUid }, null, (result) => this.render(poll, result));
|
||
}
|
||
|
||
render(poll, result) {
|
||
const byUid = {};
|
||
(result.options || []).forEach((option) => {
|
||
byUid[option.uid] = option;
|
||
});
|
||
poll.querySelectorAll(".poll-option").forEach((button) => {
|
||
const data = byUid[button.dataset.optionUid];
|
||
if (!data) {
|
||
return;
|
||
}
|
||
const chosen = result.my_choice === data.uid;
|
||
button.classList.toggle("chosen", chosen);
|
||
button.setAttribute("aria-pressed", chosen ? "true" : "false");
|
||
const bar = button.querySelector(".poll-option-bar");
|
||
if (bar) {
|
||
bar.style.setProperty("--poll-pct", `${data.pct}%`);
|
||
}
|
||
const pct = button.querySelector(".poll-option-pct");
|
||
if (pct) {
|
||
pct.textContent = `${data.pct}%`;
|
||
}
|
||
const check = button.querySelector(".poll-option-check");
|
||
if (check) {
|
||
check.hidden = !chosen;
|
||
}
|
||
});
|
||
const total = poll.querySelector(".poll-total");
|
||
if (total) {
|
||
total.textContent = `${result.total} vote${result.total === 1 ? "" : "s"}`;
|
||
}
|
||
}
|
||
|
||
toggleBuilder(toggle) {
|
||
const form = toggle.closest("form");
|
||
const builder = form ? form.querySelector("[data-poll-builder]") : null;
|
||
if (!builder) {
|
||
return;
|
||
}
|
||
const activating = builder.hidden;
|
||
builder.hidden = !activating;
|
||
this.setPollEnabled(builder, activating);
|
||
const label = toggle.querySelector("[data-poll-toggle-label]");
|
||
if (label) {
|
||
label.textContent = activating ? "Remove poll" : "Add poll";
|
||
}
|
||
}
|
||
|
||
setPollEnabled(builder, enabled) {
|
||
builder.querySelectorAll("input[name='poll_question'], input[name='poll_options']").forEach((input) => {
|
||
input.disabled = !enabled;
|
||
if (!enabled) {
|
||
input.value = "";
|
||
}
|
||
});
|
||
const error = builder.querySelector("[data-poll-error]");
|
||
if (error) {
|
||
error.hidden = true;
|
||
error.textContent = "";
|
||
}
|
||
}
|
||
|
||
addOption(button) {
|
||
const form = button.closest("form");
|
||
const container = form ? form.querySelector("[data-poll-options]") : null;
|
||
if (!container) {
|
||
return;
|
||
}
|
||
const rows = container.querySelectorAll(".poll-builder-row");
|
||
if (rows.length >= 6) {
|
||
return;
|
||
}
|
||
const row = document.createElement("div");
|
||
row.className = "poll-builder-row";
|
||
const input = document.createElement("input");
|
||
input.type = "text";
|
||
input.name = "poll_options";
|
||
input.maxLength = 100;
|
||
input.placeholder = `Option ${rows.length + 1}`;
|
||
const remove = document.createElement("button");
|
||
remove.type = "button";
|
||
remove.className = "poll-builder-remove";
|
||
remove.dataset.pollRemoveOption = "";
|
||
remove.setAttribute("aria-label", "Remove option");
|
||
remove.textContent = "×";
|
||
row.appendChild(input);
|
||
row.appendChild(remove);
|
||
container.appendChild(row);
|
||
}
|
||
|
||
removeOption(button) {
|
||
const container = button.closest("[data-poll-options]");
|
||
if (!container) {
|
||
return;
|
||
}
|
||
if (container.querySelectorAll(".poll-builder-row").length <= 2) {
|
||
return;
|
||
}
|
||
const row = button.closest(".poll-builder-row");
|
||
if (row) {
|
||
row.remove();
|
||
}
|
||
}
|
||
|
||
onSubmit(event) {
|
||
const form = event.target;
|
||
if (!form || typeof form.querySelector !== "function") {
|
||
return;
|
||
}
|
||
const builder = form.querySelector("[data-poll-builder]");
|
||
if (!builder || builder.hidden) {
|
||
return;
|
||
}
|
||
const question = builder.querySelector("input[name='poll_question']");
|
||
const options = [...builder.querySelectorAll("input[name='poll_options']")]
|
||
.map((input) => input.value.trim())
|
||
.filter(Boolean);
|
||
let problem = "";
|
||
if (!question || !question.value.trim()) {
|
||
problem = "Add a poll question, or remove the poll.";
|
||
} else if (options.length < 2) {
|
||
problem = "Add at least two poll options, or remove the poll.";
|
||
}
|
||
if (problem) {
|
||
event.preventDefault();
|
||
event.stopImmediatePropagation();
|
||
const error = builder.querySelector("[data-poll-error]");
|
||
if (error) {
|
||
error.textContent = problem;
|
||
error.hidden = false;
|
||
}
|
||
}
|
||
}
|
||
}
|