1// Small UI helpers shared by every part of the app: DOM building, toast messages and pop-up dialogs. 2import { avatarSvg } from "./avatars.js?v=24"; 3 4export const $ = (id) => document.getElementById(id); 5 6export function el(tag, className, text) { 7 const node = document.createElement(tag); 8 if (className) node.className = className; 9 if (text !== undefined) node.textContent = text; 10 return node; 11} 12 13// ---------- Toast messages ---------- 14 15const message = $("message"); 16const toast = $("toast"); 17let noticeTimer = null; 18 19// Turns Firebase error codes into something a player can understand. 20export function friendlyError(err) { 21 switch (err.code) { 22 case "permission-denied": 23 return "you don't have permission to do that, something in it isn't allowed, or you did it a lot in a short time " 24 + "(then take a short break and try again)"; 25 case "unavailable": 26 return "can't reach the server. Check your internet connection"; 27 case "auth/popup-closed-by-user": 28 case "auth/cancelled-popup-request": 29 return "the sign-in window was closed before finishing"; 30 case "auth/popup-blocked": 31 return "your browser blocked the sign-in window. Allow pop-ups for this site and try again"; 32 case "auth/invalid-action-code": 33 case "auth/expired-action-code": 34 return "that link has expired or was already used. Ask for a new one"; 35 case "auth/invalid-email": 36 return "that email address doesn't look right"; 37 case "auth/quota-exceeded": 38 return "too many sign-in emails were sent today. Try again tomorrow, or use Google sign-in"; 39 default: 40 return err.code || err.message; 41 } 42} 43 44// How long messages stay up. They can also be closed with Ã, and switching tabs clears them (app.js). 45const PROBLEM_MS = 6000; 46const NOTICE_MS = 3000; 47 48// A red message that hides itself after a few seconds. 49export function showProblem(text) { 50 clearTimeout(noticeTimer); 51 message.classList.remove("ok"); 52 toast.classList.remove("ok"); 53 message.textContent = text; 54 toast.classList.toggle("show", !!text); 55 if (text) noticeTimer = setTimeout(clearMessage, PROBLEM_MS); 56} 57 58export function showError(prefix, err) { 59 showProblem(`${prefix}: ${friendlyError(err)}`); 60} 61 62export function clearMessage() { 63 showProblem(""); 64} 65 66// A short green message that disappears by itself. 67export function showNotice(text) { 68 showProblem(text); 69 message.classList.add("ok"); 70 toast.classList.add("ok"); 71 clearTimeout(noticeTimer); 72 noticeTimer = setTimeout(clearMessage, NOTICE_MS); 73} 74 75$("message-close").addEventListener("click", clearMessage); 76 77// ---------- Pop-up dialogs ---------- 78 79const dialog = $("dialog"); 80const dialogTitle = $("dialog-title"); 81const dialogText = $("dialog-text"); 82const dialogBody = $("dialog-body"); 83const dialogConfirm = $("dialog-confirm"); 84const dialogCancel = $("dialog-cancel"); 85let dialogResolve = null; 86 87function closeDialog(result) { 88 if (!dialogResolve) return; 89 const resolve = dialogResolve; 90 dialogResolve = null; 91 dialog.close(); 92 resolve(result); 93} 94 95dialogConfirm.addEventListener("click", (event) => { 96 event.preventDefault(); 97 closeDialog(true); 98}); 99dialogCancel.addEventListener("click", () => closeDialog(false)); 100// Escape key or tapping outside the box counts as Cancel. 101dialog.addEventListener("cancel", (event) => { 102 event.preventDefault(); 103 closeDialog(false); 104}); 105dialog.addEventListener("click", (event) => { 106 if (event.target === dialog) closeDialog(false); 107}); 108 109// Opens a styled pop-up. Resolves true for the main button, false for Cancel. 110// `body` is an optional element (a text box, a checklist...) shown under the text. 111export function openDialog({ title, text = "", body = null, confirmText = "OK", cancelText = "Cancel", danger = false }) { 112 if (dialogResolve) closeDialog(false); 113 dialogTitle.textContent = title; 114 dialogText.textContent = text; 115 dialogText.hidden = !text; 116 dialogBody.replaceChildren(...(body ? [body] : [])); 117 dialogConfirm.textContent = confirmText; 118 dialogConfirm.className = danger ? "btn btn-danger" : "btn btn-primary"; 119 dialogCancel.textContent = cancelText; 120 dialog.showModal(); 121 (body?.querySelector("input, textarea") || dialogConfirm).focus(); 122 return new Promise((resolve) => { dialogResolve = resolve; }); 123} 124 125// Yes/no question. Resolves true or false. 126export function askConfirm({ title, text, confirmText = "Yes", danger = false }) { 127 return openDialog({ title, text, confirmText, danger }); 128} 129 130// Asks for some text. Resolves with the text (maybe empty), or null if cancelled. 131export async function askText({ title, text, placeholder = "", confirmText = "Send", maxLength = 200 }) { 132 const input = el("textarea", "dialog-input"); 133 input.id = "dialog-input"; 134 input.rows = 3; 135 input.maxLength = maxLength; 136 input.placeholder = placeholder; 137 const ok = await openDialog({ title, text, body: input, confirmText }); 138 return ok ? input.value.trim() : null; 139} 140
141// A strong "are you sure?" for things that can't be undone: the player has to type a word (like DELETE). 142// Resolves true if they typed it (upper or lower case), false if they typed something else, null if cancelled. 143export async function askTyped({ title, text, expected, confirmText }) { 144 const input = el("input", "dialog-input"); 145 input.id = "dialog-input"; 146 input.autocomplete = "off"; 147 input.autocapitalize = "off"; 148 input.spellcheck = false; 149 input.placeholder = expected; 150 input.setAttribute("aria-label", `Type ${expected} to confirm`); 151 const ok = await openDialog({ title, text, body: input, confirmText, danger: true }); 152 if (!ok) return null; 153 return input.value.trim().toLowerCase() === expected.toLowerCase(); 154} 155 156// ---------- Little shared bits ---------- 157 158export function timeAgo(timestamp) { 159 if (!timestamp) return "just now"; 160 const seconds = (Date.now() - timestamp.toMillis()) / 1000; 161 if (seconds < 60) return "just now"; 162 if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; 163 if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; 164 return `${Math.floor(seconds / 86400)}d ago`; 165} 166 167// A friendly "nothing here" box with a pixel buddy. 168export function emptyState(avatar, title, text) { 169 const box = el("div", "empty"); 170 box.append(avatarSvg(avatar, 48), el("p", "empty-title", title), el("p", "", text)); 171 return box; 172}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.