Something went wrong. Try again.
Web frontend and supporting services for lance.blue
Something went wrong. Try again.
5.1 kB · 155 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156/** * Report an issue: the site's version of the match screen's flare button. * * One dialog, opened from the account menu. It files the same * `app.userinput.discussion` the match screen does — POST /api/flare, written * to the player's own repository, aimed at the lance.blue board on * userinput.app — but with no match to name and no canvas to capture, so it * is text alone. * * Loaded on the click that opens it, like the sign-in dialog: the account * menu is on every page, the blog's included, and nobody who does not report * anything should pay for this. * * Unlike the sign-in dialog there is no typeahead in here eating keydowns, so * Escape is left to the dialog itself: `cancel` closes it, and the `close` * listener below removes it, whichever way it ended. */
import { ApiError, currentSession, sendFlare } from "./api";import { el } from "./dom";
/** * The lance.blue board, as userinput.app serves it. The `at://` form of the * same space is the API's FLARE_SPACE, set in infra — a report filed here * lands on this page. */const BOARD_URL = "https://userinput.app/s/did:plc:a2j2g42ai6v65qpbvb6hmubi/3msr5yrvtq22g";
/** * What a report can be tagged with — the board's own list, written down * rather than fetched. The board is ours and its tags change about as often * as this file does; arena's flare dialog carries the same four. */const TAGS = ["bug", "feature", "question", "thanks"];
/** * One tag, as a checkbox with its label beside it. The label wraps the box, * so the word is part of the hit target and names the control without an id * for anything to point at. */function tagBox(tag: string): HTMLLabelElement { const box = el("input", { type: "checkbox", value: tag }); return el("label", { className: "report-tag" }, [box, tag]);}
/** A new tab to the board, from the notice and from a sent report. */function boardLink(text: string, href: string): HTMLAnchorElement { return el("a", { href, textContent: text, target: "_blank", rel: "noopener", });}
/** Puts the report dialog up and focuses the title field. */export function openReport(): void { const headingId = "report-heading";
const title = el("input", { type: "text", maxLength: 300, placeholder: "What went wrong, in one line", "aria-label": "What went wrong, in one line", }); const body = el("textarea", { rows: 5, placeholder: "What were you doing, and what did you expect instead?", "aria-label": "What were you doing, and what did you expect instead?", });
const tagRow = el("div", { className: "report-tags" }, TAGS.map(tagBox)); const ticked = (): string[] => [...tagRow.querySelectorAll<HTMLInputElement>("input:checked")].map( (box) => box.value, );
const status = el("p", { className: "hint", role: "alert" }); const cancel = el("button", { type: "button", className: "secondary", textContent: "Not now", }); const send = el("button", { type: "submit", textContent: "Send report" });
const form = el( "form", // ph-no-capture for the same reason as the sign-in form: what a player // types here is theirs until they press send. { className: "signin ph-no-capture report" }, [ el("h2", { id: headingId, textContent: "Report an issue" }), title, body, tagRow, el("p", { className: "signin-blurb" }, [ "This posts publicly to the ", boardLink("lance.blue feedback board", BOARD_URL), ", from your own account.", ]), status, el("div", { className: "signin-actions" }, [send, cancel]), ], );
const dialog = el( "dialog", { className: "modal", "aria-labelledby": headingId }, [form], ); const close = () => dialog.close(); cancel.addEventListener("click", close); dialog.addEventListener("close", () => dialog.remove()); dialog.addEventListener("click", (event) => { if (event.target === dialog) close(); });
form.addEventListener("submit", (event) => { event.preventDefault(); if (send.disabled) return; send.disabled = true; status.className = "hint"; status.textContent = "Sending…";
sendFlare(title.value, body.value, ticked()) .then(async (written) => { status.textContent = "Sent. "; // The report is a record in the player's own repository, so the // board's page for it is named by their DID and the new rkey. const session = await currentSession().catch(() => null); if (session && written.rkey) { status.append( boardLink( "View it on the board.", `https://userinput.app/d/${session.did}/${written.rkey}`, ), ); } window.setTimeout(close, 4000); }) .catch((err: unknown) => { send.disabled = false; status.className = "error"; status.textContent = err instanceof ApiError ? err.message : "The report could not be sent."; }); });
document.body.append(dialog); dialog.showModal(); title.focus();}