export function byId(id: string): T { const node = document.getElementById(id); if (!node) throw new Error(`missing #${id}`); return node as T; } export function node( tag: K, className?: string, text?: string, ): HTMLElementTagNameMap[K] { const element = document.createElement(tag); if (className) element.className = className; if (text !== undefined) element.textContent = text; return element; } export function clear(element: HTMLElement): void { element.replaceChildren(); } export function groupDigits(amount: number): string { const negative = amount < 0; const digits = String(Math.abs(Math.trunc(amount))); let grouped = ""; for (let index = 0; index < digits.length; index += 1) { if (index > 0 && (digits.length - index) % 3 === 0) grouped += "\u2009"; grouped += digits[index]; } return negative ? `-${grouped}` : grouped; } export function money(amount: number | null | undefined): string { if (amount === null || amount === undefined || Number.isNaN(amount)) return "— Kr"; return `${groupDigits(amount)} Kr`; } export function signedMoney(amount: number): string { return `${amount < 0 ? "-" : "+"}${money(Math.abs(amount))}`; } export function toast(text: string, tone: "info" | "good" | "bad" = "info"): void { const root = document.getElementById("toasts"); if (!root || !text) return; const item = node("div", `toast ${tone}`, text); root.append(item); window.setTimeout(() => { item.style.opacity = "0"; item.style.transition = "opacity 220ms ease"; window.setTimeout(() => item.remove(), 240); }, 4200); while (root.children.length > 4) root.firstElementChild?.remove(); } export function toneFor(text: string): "info" | "good" | "bad" { const lowered = text.toLowerCase(); if ( text.startsWith("ERROR") || lowered.includes("failed") || lowered.includes("lost") || lowered.includes("invalid") || lowered.includes("forfeit") ) { return "bad"; } if ( text.startsWith("Logged in") || text.startsWith("Account created") || lowered.includes("won") || lowered.includes("cashed") ) { return "good"; } return "info"; } export async function sha256Hex(text: string): Promise { const bytes = new TextEncoder().encode(text); const digest = await crypto.subtle.digest("SHA-256", bytes); return [...new Uint8Array(digest)] .map((byte) => byte.toString(16).padStart(2, "0")) .join(""); } export function shortHash(hash: string, keep = 10): string { if (hash.length <= keep * 2) return hash; return `${hash.slice(0, keep)}…${hash.slice(-keep)}`; } export function formatTime(raw: string): string { // SQLite stores `YYYY-MM-DD HH:MM:SS` in UTC. const parsed = Date.parse(raw.includes("T") ? raw : raw.replace(" ", "T") + "Z"); if (Number.isNaN(parsed)) return raw; return new Date(parsed).toLocaleString(); } export function cardFace(card: string): { rank: string; suit: string; red: boolean } { if (card === "?" || !card) return { rank: "?", suit: "", red: false }; const suit = card.slice(-1); const rank = card.slice(0, -1); const glyph = suit === "C" ? "♣" : suit === "D" ? "♦" : suit === "H" ? "♥" : suit === "S" ? "♠" : suit; return { rank, suit: glyph, red: suit === "D" || suit === "H" }; }