// fetch helpers + session state + toasts export const state = { me: null, catalog: [], }; export async function api(path, opts = {}) { const res = await fetch('/api' + path, { headers: opts.body ? { 'Content-Type': 'application/json' } : {}, method: opts.method || (opts.body ? 'POST' : 'GET'), body: opts.body ? JSON.stringify(opts.body) : undefined, }); let data = {}; try { data = await res.json(); } catch (e) { /* non-JSON */ } if (!res.ok) { const err = new Error(data.error || `Request failed (${res.status})`); err.status = res.status; throw err; } return data; } export async function refreshMe() { const data = await api('/me'); state.me = data.user; if (data.dailyBonus) toast(`Daily bonus: +${data.dailyBonus} ◈ Glimmers!`, 'gold'); return state.me; } export async function loadCatalog() { if (!state.catalog.length) { const data = await api('/catalog'); state.catalog = data.items; } return state.catalog; } export function catalogItem(id) { return state.catalog.find((i) => i.id === id) || null; } export function toast(text, kind = '') { const el = document.createElement('div'); el.className = 'toast ' + kind; el.textContent = text; document.getElementById('toasts').appendChild(el); setTimeout(() => el.remove(), 3500); } export function esc(s) { return String(s ?? '').replace(/[&<>"']/g, (c) => ( { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c] )); } export function fmtNum(n) { if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M'; if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K'; return String(n); } export function openWS() { const proto = location.protocol === 'https:' ? 'wss' : 'ws'; return new WebSocket(`${proto}://${location.host}/ws`); }