Files
jimi-gallery/assets/data.js
yakenator 098b55e3b0 feat: initial Jimi Gallery prototype
- Public site (Home/Artists/Exhibitions/News/About/Contact) with EN/KO/JA i18n
- Admin panel with login, CRUD, image upload, multilingual editing
- Exhibition slider/lightbox view
- FastAPI + MongoDB backend, JWT auth
- Docker Compose deployment, behind nginx at jimi.yakenator.io
2026-04-25 12:47:36 +09:00

200 lines
5.2 KiB
JavaScript

// 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 = `
<main style="padding:60px;max-width:640px;margin:48px auto;font-family:'Inter',system-ui,sans-serif;line-height:1.55;color:#111">
<h1 style="color:#b00020;font-size:22px;margin:0 0 12px">Backend unreachable</h1>
<p style="margin:0 0 14px">Make sure the API + Mongo stack is running:</p>
<pre style="background:#f4f4f4;padding:12px;font-size:12px;overflow:auto;margin:0 0 14px">docker compose up</pre>
<p style="margin:0 0 14px">Then reload this page.</p>
<p style="color:#999;font-size:12px;margin:0">${msg}</p>
</main>
`;
}
window.Store = Store;
window.Auth = Auth;
window.showBootError = showBootError;