import { api, protectedFetch } from "./api.js?v=__ASSET_VERSION__"; import { initSideNav, loadNavUser } from "./nav.js?v=__ASSET_VERSION__"; import { showToast } from "./toast.js?v=__ASSET_VERSION__"; import { BREW_CATEGORIES, BREW_METHODS, BREW_SILHOUETTES, } from "/shared/brew-data.js?v=__ASSET_VERSION__"; const SVG_NS = "http://www.w3.org/2000/svg"; let gear = { brewers: [], grinders: [] }; let saveTimer = null; // Never save over the server until we've successfully read its state: the PUT replaces both // arrays wholesale, so saving after a failed load would wipe the user's stored equipment. let loaded = false; function silhouetteSvg(methodKey) { const svg = document.createElementNS(SVG_NS, "svg"); svg.setAttribute("viewBox", "0 0 64 64"); svg.setAttribute("width", 42); svg.setAttribute("height", 42); svg.setAttribute("aria-hidden", "true"); for (const d of BREW_SILHOUETTES[methodKey] ?? []) { const path = document.createElementNS(SVG_NS, "path"); path.setAttribute("d", d); path.setAttribute("fill", "currentColor"); svg.append(path); } return svg; } // Saves shortly after the last toggle rather than needing a Save button — an equipment page // is set-and-forget, so every change should just stick. function scheduleSave() { if (!loaded) { showToast("Equipment didn't load — reload the page before making changes.", "fail"); return; } clearTimeout(saveTimer); saveTimer = setTimeout(async () => { try { const body = await api("/api/gear", { method: "PUT", body: JSON.stringify(gear), }); gear = body.gear; showToast("Equipment saved."); } catch (error) { showToast(error.message, "fail"); } }, 500); } function renderCount() { document.getElementById("brewers-count").textContent = gear.brewers.length ? `${gear.brewers.length} selected` : "none selected — Brews shows all"; } function renderBrewerPicker() { const mount = document.getElementById("gear-brewer-picker"); mount.replaceChildren(); for (const category of BREW_CATEGORIES) { const title = document.createElement("div"); title.className = "brewer-cat-title"; title.textContent = category.name; mount.append(title); const grid = document.createElement("div"); grid.className = "brewer-grid"; for (const method of BREW_METHODS.filter((m) => m.category === category.key)) { const tile = document.createElement("button"); tile.type = "button"; tile.className = "brewer-tile"; const owned = () => gear.brewers.includes(method.key); tile.classList.toggle("selected", owned()); tile.setAttribute("aria-pressed", String(owned())); tile.append(silhouetteSvg(method.key)); const name = document.createElement("span"); name.className = "brewer-name"; name.textContent = method.name; tile.append(name); tile.addEventListener("click", () => { gear.brewers = owned() ? gear.brewers.filter((k) => k !== method.key) : [...gear.brewers, method.key]; tile.classList.toggle("selected", owned()); tile.setAttribute("aria-pressed", String(owned())); renderCount(); scheduleSave(); }); grid.append(tile); } mount.append(grid); } renderCount(); } function renderGrinders() { const list = document.getElementById("grinder-list"); if (!gear.grinders.length) { list.replaceChildren( Object.assign(document.createElement("li"), { textContent: "No grinders yet.", className: "muted", }), ); return; } list.replaceChildren( ...gear.grinders.map((grinder) => { const li = document.createElement("li"); li.style.display = "flex"; li.style.alignItems = "center"; li.style.gap = "10px"; const name = document.createElement("span"); name.textContent = grinder; const remove = document.createElement("button"); remove.className = "ghost-btn small"; remove.type = "button"; remove.textContent = "Remove"; remove.addEventListener("click", () => { gear.grinders = gear.grinders.filter((g) => g !== grinder); renderGrinders(); scheduleSave(); }); li.append(name, remove); return li; }), ); } document.getElementById("grinder-form").addEventListener("submit", (event) => { event.preventDefault(); const input = document.getElementById("grinder-name"); const name = input.value.trim(); if (!name) return; if (!gear.grinders.includes(name)) gear.grinders = [...gear.grinders, name]; input.value = ""; renderGrinders(); scheduleSave(); }); document.getElementById("btn-logout").addEventListener("click", async () => { await protectedFetch("/api/auth/logout", { method: "POST" }); location.assign("/"); }); async function init() { initSideNav(); const user = await loadNavUser(); if (!user) return; try { gear = (await api("/api/gear")).gear; loaded = true; } catch { showToast("Could not load equipment.", "fail"); } renderBrewerPicker(); renderGrinders(); } init();