async function api(method, path, body) { const opts = { method, headers: { "Content-Type": "application/json" }, credentials: "include", }; if (body !== undefined) opts.body = JSON.stringify(body); const res = await fetch(apiURL(path), opts); if (res.status === 204) return null; let data = null; const text = await res.text(); if (text) { try { data = JSON.parse(text); } catch (e) { data = text; } } if (!res.ok) { const err = new Error((data && data.error) || res.statusText); err.status = res.status; err.data = data; throw err; } return data; } const API = { login: (u, p) => api("POST", "/api/login", { username: String(u || "").toLowerCase().trim(), password: p }), logout: () => api("POST", "/api/logout"), me: () => api("GET", "/api/me"), updateMe: (b) => api("PATCH", "/api/me", b), listUsers: () => api("GET", "/api/users"), createUser: (b) => api("POST", "/api/users", b), updateUser: (id, b) => api("PATCH", `/api/users/${id}`, b), deleteUser: (id) => api("DELETE", `/api/users/${id}`), listCompetitions: () => api("GET", "/api/competitions"), createCompetition: (b) => api("POST", "/api/competitions", b), getCompetition: (id) => api("GET", `/api/competitions/${id}`), updateCompetition: (id, b) => api("PATCH", `/api/competitions/${id}`, b), deleteCompetition: (id) => api("DELETE", `/api/competitions/${id}`), listMembers: (id) => api("GET", `/api/competitions/${id}/members`), addMember: (id, b) => api("POST", `/api/competitions/${id}/members`, b), removeMember: (id, uid) => api("DELETE", `/api/competitions/${id}/members/${uid}`), listPilots: (id) => api("GET", `/api/competitions/${id}/pilots`), createPilot: (id, b) => api("POST", `/api/competitions/${id}/pilots`, b), updatePilot: (id, pid, b) => api("PATCH", `/api/competitions/${id}/pilots/${pid}`, b), deletePilot: (id, pid) => api("DELETE", `/api/competitions/${id}/pilots/${pid}`), importPilots: async (id, csv) => { const res = await fetch(apiURL(`/api/competitions/${id}/pilots/import`), { method: "POST", headers: { "Content-Type": "text/csv" }, body: csv, credentials: "include", }); if (!res.ok) throw new Error("import_failed"); return res.json(); }, listPenalties: (id) => api("GET", `/api/competitions/${id}/penalties`), createPenalty: (id, b) => api("POST", `/api/competitions/${id}/penalties`, b), updatePenalty: (id, pid, b) => api("PATCH", `/api/competitions/${id}/penalties/${pid}`, b), deletePenalty: (id, pid) => api("DELETE", `/api/competitions/${id}/penalties/${pid}`), applyPenalties: (id, b) => api("POST", `/api/competitions/${id}/penalties/apply`, b), exportPenaltiesCSV: async (id) => { const res = await fetch(apiURL(`/api/competitions/${id}/penalties.csv`), { credentials: "include" }); if (!res.ok) throw new Error("export_failed"); return res.blob(); }, listRules: (lang) => api("GET", `/api/rules${lang ? "?lang=" + encodeURIComponent(lang) : ""}`), listRuleLanguages: () => api("GET", "/api/rules/languages"), }; function openCompetitionWS(id, handlers) { const url = wsURL(`/api/competitions/${id}/ws`); let ws = null; let closed = false; let backoff = 1000; function connect() { ws = new WebSocket(url); ws.onopen = () => { backoff = 1000; if (handlers.onopen) handlers.onopen(); }; ws.onmessage = (e) => { try { const msg = JSON.parse(e.data); if (handlers.onmessage) handlers.onmessage(msg); } catch (err) {} }; ws.onclose = () => { if (handlers.onclose) handlers.onclose(); if (!closed) { setTimeout(connect, backoff); backoff = Math.min(backoff * 2, 15000); } }; ws.onerror = () => { try { ws.close(); } catch (e) {} }; } connect(); return { close: () => { closed = true; try { ws && ws.close(); } catch (e) {} } }; }