PageSourceSearch

https://1gtrack.pages.dev/ui.js?v=24

js 1gtrack.pages.dev collected 2026-10-03 05:37:53 UTC 6,315 bytes, 172 lines download raw bytes

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.