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
This commit is contained in:
199
assets/data.js
Normal file
199
assets/data.js
Normal file
@ -0,0 +1,199 @@
|
||||
// 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;
|
||||
Reference in New Issue
Block a user