// Gallery data store — backed by the FastAPI + MongoDB backend. // // Pattern: preload once (await Store.load()), then all accessors are sync // reads from the in-memory cache. Mutators (upsert/remove/updateSettings/ // reset/importAll) hit the API and invalidate the cache. // // Pages should wrap their inline script in: // (async () => { // await Store.load(); // renderChrome(...); // // ... rest of rendering // })(); const API_BASE = ""; const AUTH_KEY = "jimi_gallery_admin_token_v1"; // Legacy localStorage key used by the prototype — unused now but kept here // as a reference in case a migration path is ever needed. const LEGACY_STORE_KEY = "jimi_gallery_store_v2"; async function apiGet(path) { const r = await fetch(API_BASE + path, { method: "GET" }); if (!r.ok) throw new Error(`GET ${path} → ${r.status}`); return r.json(); } async function apiJson(path, method, body, authed) { const headers = { "Content-Type": "application/json" }; if (authed) { const tok = localStorage.getItem(AUTH_KEY); if (tok) headers["Authorization"] = "Bearer " + tok; } const r = await fetch(API_BASE + path, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) }); if (r.status === 401 && authed) { localStorage.removeItem(AUTH_KEY); // Bounce to login if we're in the admin area. if (location.pathname.includes("/admin/")) { location.href = "index.html"; } throw new Error("unauthorized"); } if (!r.ok) { let detail = `${method} ${path} → ${r.status}`; try { const j = await r.json(); if (j && j.detail) detail = String(j.detail); } catch (e) {} throw new Error(detail); } if (r.status === 204) return null; return r.json(); } const Store = { _cache: null, _loadPromise: null, async load() { if (this._cache) return this._cache; if (this._loadPromise) return this._loadPromise; this._loadPromise = Promise.all([ apiGet("/api/settings"), apiGet("/api/artists"), apiGet("/api/exhibitions"), apiGet("/api/news") ]) .then(([settings, artists, exhibitions, news]) => { this._cache = { settings, artists, exhibitions, news }; return this._cache; }) .finally(() => { this._loadPromise = null; }); return this._loadPromise; }, async reload() { this._cache = null; return this.load(); }, // --- sync accessors (assume cache is warm) --- settings() { return (this._cache && this._cache.settings) || {}; }, artists() { return (this._cache && this._cache.artists) || []; }, artist(id) { return this.artists().find((a) => a.id === id); }, exhibitions() { return (this._cache && this._cache.exhibitions) || []; }, exhibition(id) { return this.exhibitions().find((e) => e.id === id); }, news() { return (this._cache && this._cache.news) || []; }, newsItem(id) { return this.news().find((n) => n.id === id); }, // --- async mutators --- async upsert(collection, item) { if (!item || !item.id) throw new Error("id required"); await apiJson( `/api/${collection}/${encodeURIComponent(item.id)}`, "PUT", item, true ); await this.reload(); }, async remove(collection, id) { await apiJson( `/api/${collection}/${encodeURIComponent(id)}`, "DELETE", undefined, true ); await this.reload(); }, async updateSettings(patch) { await apiJson("/api/settings", "PUT", patch, true); await this.reload(); }, async reset() { await apiJson("/api/admin/seed", "POST", {}, true); await this.reload(); }, async exportAll() { return apiGet("/api/admin/export"); }, async importAll(data) { await apiJson("/api/admin/import", "POST", data, true); await this.reload(); } }; const Auth = { async login(password) { try { const data = await apiJson( "/api/auth/login", "POST", { password }, false ); if (data && data.token) { localStorage.setItem(AUTH_KEY, data.token); return true; } } catch (e) { // fall through } return false; }, logout() { localStorage.removeItem(AUTH_KEY); }, // Sync check — we trust the token's presence; the API will 401 and apiJson // redirects to login when it's actually expired. isAuthed() { return !!localStorage.getItem(AUTH_KEY); }, token() { return localStorage.getItem(AUTH_KEY); } }; function showBootError(e) { const msg = (e && e.message) || String(e); document.body.innerHTML = `

Backend unreachable

Make sure the API + Mongo stack is running:

docker compose up

Then reload this page.

${msg}

`; } window.Store = Store; window.Auth = Auth; window.showBootError = showBootError;