Spaces:
Running
Running
| // 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`); | |
| } | |