// Shared helpers for the admin panel. function requireAuth() { if (!Auth.isAuthed()) { location.href = "index.html"; } } function slugify(s) { return (s || "") .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); } function uid(prefix) { return prefix + "-" + Math.random().toString(36).slice(2, 8); } const ADMIN_LOGO_SVG = ` `; function sidebarHtml(active) { const items = [ ["dashboard.html", t("admin.sidebar.dashboard"), "dashboard"], ["artists.html", t("admin.sidebar.artists"), "artists"], ["exhibitions.html", t("admin.sidebar.exhibitions"), "exhibitions"], ["news.html", t("admin.sidebar.news"), "news"], ["settings.html", t("admin.sidebar.settings"), "settings"] ]; const curLang = getLang(); return ` `; } function mountSidebar(active) { const host = document.getElementById("sidebar-slot"); if (host) host.outerHTML = sidebarHtml(active); document.getElementById("sidebar-lang")?.addEventListener("change", (e) => { setLang(e.target.value); }); document.getElementById("lang-reset")?.addEventListener("click", (e) => { e.preventDefault(); clearLangOverride(); }); document.getElementById("logout")?.addEventListener("click", (e) => { e.preventDefault(); Auth.logout(); location.href = "index.html"; }); document.getElementById("reset-data")?.addEventListener("click", async (e) => { e.preventDefault(); if (!confirm(t("admin.confirm.reset"))) return; try { await Store.reset(); toast(t("admin.toast.data_reset")); setTimeout(() => location.reload(), 400); } catch (err) { alert(err.message || err); } }); document.getElementById("export-data")?.addEventListener("click", (e) => { e.preventDefault(); exportData(); }); document.getElementById("import-data")?.addEventListener("click", (e) => { e.preventDefault(); document.getElementById("import-file")?.click(); }); document.getElementById("import-file")?.addEventListener("change", (e) => { const f = e.target.files && e.target.files[0]; if (f) importDataFromFile(f); e.target.value = ""; }); } async function exportData() { let data; try { data = await Store.exportAll(); } catch (e) { alert(e.message || e); return; } const json = JSON.stringify(data, null, 2); const blob = new Blob([json], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; const ts = new Date().toISOString().slice(0, 10); a.download = `jimi-gallery-${ts}.json`; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); toast(t("admin.toast.exported")); } function importDataFromFile(file) { const reader = new FileReader(); reader.onerror = () => alert(t("admin.error.invalid_file")); reader.onload = async () => { let parsed; try { parsed = JSON.parse(reader.result); } catch (e) { alert(t("admin.error.invalid_file")); return; } if ( !parsed || typeof parsed !== "object" || !parsed.settings || !Array.isArray(parsed.artists) || !Array.isArray(parsed.exhibitions) || !Array.isArray(parsed.news) ) { alert(t("admin.error.invalid_file")); return; } if (!confirm(t("admin.confirm.import"))) return; try { await Store.importAll(parsed); toast(t("admin.toast.imported")); setTimeout(() => location.reload(), 400); } catch (err) { alert(err.message || err); } }; reader.readAsText(file); } let _toastTimer; function toast(msg) { let el = document.querySelector(".toast"); if (!el) { el = document.createElement("div"); el.className = "toast"; document.body.appendChild(el); } el.textContent = msg; requestAnimationFrame(() => el.classList.add("show")); clearTimeout(_toastTimer); _toastTimer = setTimeout(() => el.classList.remove("show"), 1800); } function openModal() { document.getElementById("modal")?.classList.add("open"); } function closeModal() { document.getElementById("modal")?.classList.remove("open"); } function fmtDateShort(iso) { if (!iso) return "—"; return new Date(iso + "T00:00:00").toLocaleDateString( typeof getLocale === "function" ? getLocale() : "en-US", { month: "short", day: "numeric", year: "numeric" } ); } // Mount a multilingual text field with EN/KO/JA tabs. Returns a handle with // .get() returning the { en, ko, ja } object. Pair this with form submit: // const bio = mountMultilingualField(el, { value: a.bio }); // // later: item.bio = bio.get(); function mountMultilingualField(container, opts = {}) { const { value = {}, type = "textarea", placeholder = "", minHeight = "" } = opts; const initial = typeof value === "string" ? { en: value, ko: "", ja: "" } : { en: (value && value.en) || "", ko: (value && value.ko) || "", ja: (value && value.ja) || "" }; const state = { ...initial }; const langs = SUPPORTED_LANGS; let current = getLang(); if (!langs.includes(current)) current = "en"; container.classList.add("ml-field"); const inputEl = type === "input" ? `` : ``; container.innerHTML = `