Files
jimi-gallery/admin/exhibitions.html
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

233 lines
9.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Exhibitions — Jimi Gallery Admin</title>
<link
href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;600&family=Cormorant+Garamond:wght@400;500;600&family=Inter:wght@300;400;500&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="admin.css" />
</head>
<body>
<div class="app">
<div id="sidebar-slot"></div>
<div class="content">
<div class="topbar">
<div>
<h1 id="pg-title"></h1>
<div class="subtitle" id="pg-sub"></div>
</div>
<button class="btn" id="new-btn"></button>
</div>
<div class="panel" style="padding:0">
<table class="table" id="tbl"></table>
</div>
</div>
</div>
<div class="modal" id="modal">
<div class="body">
<h2>
<span id="modal-title"></span>
<button class="close" onclick="closeModal()">×</button>
</h2>
<form id="form" class="form">
<input type="hidden" id="f-id" />
<div class="full">
<label id="lbl-title"></label>
<div id="f-title-wrap"></div>
</div>
<div>
<label id="lbl-status"></label>
<select id="f-status">
<option value="current"></option>
<option value="upcoming"></option>
<option value="past"></option>
</select>
</div>
<div>
<label id="lbl-venue"></label>
<input id="f-venue" />
</div>
<div>
<label id="lbl-start"></label>
<input id="f-start" type="date" />
</div>
<div>
<label id="lbl-end"></label>
<input id="f-end" type="date" />
</div>
<div class="full">
<label id="lbl-hero"></label>
<div id="f-hero-wrap"></div>
</div>
<div class="full">
<label id="lbl-artists"></label>
<select id="f-artists" multiple size="6"></select>
</div>
<div class="full">
<label id="lbl-desc"></label>
<div id="f-desc-wrap"></div>
</div>
<div class="full">
<label id="lbl-press"></label>
<div id="f-press-wrap"></div>
</div>
<div class="actions">
<button type="button" class="btn ghost" id="btn-cancel" onclick="closeModal()"></button>
<button type="submit" class="btn" id="btn-save"></button>
</div>
</form>
</div>
</div>
<script src="../assets/i18n.js"></script>
<script src="../assets/data.js"></script>
<script src="admin.js"></script>
<script>
(async () => {
requireAuth();
try { await Store.load(); } catch (e) { return showBootError(e); }
mountSidebar("exhibitions");
document.getElementById("pg-title").textContent = t("admin.page.exhibitions");
document.getElementById("pg-sub").textContent = t("admin.page.exhibitions_sub");
document.getElementById("new-btn").textContent = t("admin.btn.new_exhibition");
document.getElementById("lbl-title").textContent = t("admin.form.title");
document.getElementById("lbl-status").textContent = t("admin.form.status");
document.getElementById("lbl-venue").textContent = t("admin.form.venue");
document.getElementById("lbl-start").textContent = t("admin.form.start_date");
document.getElementById("lbl-end").textContent = t("admin.form.end_date");
document.getElementById("lbl-hero").textContent = t("admin.form.hero_image");
document.getElementById("lbl-artists").innerHTML = `${t("admin.form.artists")} <span style="color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">${t("admin.form.artists_hint")}</span>`;
document.getElementById("lbl-desc").textContent = t("admin.form.description");
document.getElementById("lbl-press").textContent = t("admin.form.press_note");
document.getElementById("btn-cancel").textContent = t("admin.btn.cancel");
document.getElementById("btn-save").textContent = t("admin.btn.save");
// status select options
const statusSel = document.getElementById("f-status");
statusSel.options[0].textContent = t("status.current");
statusSel.options[1].textContent = t("status.upcoming");
statusSel.options[2].textContent = t("status.past");
let fields = {};
const order = { current: 0, upcoming: 1, past: 2 };
function render() {
const rows = [...Store.exhibitions()]
.sort((a, b) =>
order[a.status] !== order[b.status]
? order[a.status] - order[b.status]
: b.startDate.localeCompare(a.startDate)
)
.map((e) => {
const artists = e.artistIds.map((id) => Store.artist(id)?.name).filter(Boolean).join(", ");
return `
<tr>
<td><img src="${e.hero}" alt=""></td>
<td><em>${L(e.title)}</em></td>
<td>${artists}</td>
<td>${fmtDateShort(e.startDate)} ${fmtDateShort(e.endDate)}</td>
<td><span class="badge badge-${e.status}">${t("status." + e.status)}</span></td>
<td class="actions">
<a class="btn ghost btn-small" href="../exhibition.html?id=${e.id}" target="_blank">${t("admin.btn.view")}</a>
<button class="btn ghost btn-small" onclick="edit('${e.id}')">${t("admin.btn.edit")}</button>
<button class="btn danger btn-small" onclick="del('${e.id}')">${t("admin.btn.delete")}</button>
</td>
</tr>
`;
})
.join("");
document.getElementById("tbl").innerHTML = `
<thead><tr><th></th><th>${t("admin.table.title")}</th><th>${t("admin.table.artists")}</th><th>${t("admin.table.dates")}</th><th>${t("admin.table.status")}</th><th></th></tr></thead>
<tbody>${rows || `<tr><td colspan="6" style="text-align:center;padding:24px;color:var(--muted)">${t("admin.empty.exhibitions")}</td></tr>`}</tbody>
`;
}
render();
function fillArtistOptions(selectedIds = []) {
const sel = document.getElementById("f-artists");
sel.innerHTML = Store.artists()
.map(
(a) =>
`<option value="${a.id}" ${selectedIds.includes(a.id) ? "selected" : ""}>${a.name}</option>`
)
.join("");
}
window.edit = function (id) {
const e = id ? Store.exhibition(id) : null;
document.getElementById("modal-title").textContent = e
? t("admin.modal.edit_exhibition")
: t("admin.modal.new_exhibition");
document.getElementById("f-id").value = e?.id || "";
document.getElementById("f-status").value = e?.status || "upcoming";
document.getElementById("f-venue").value = e?.venue || "145 Grand Street";
document.getElementById("f-start").value = e?.startDate || "";
document.getElementById("f-end").value = e?.endDate || "";
fields.title = mountMultilingualField(document.getElementById("f-title-wrap"), {
type: "input",
value: e?.title || ""
});
fields.description = mountMultilingualField(document.getElementById("f-desc-wrap"), {
type: "textarea",
value: e?.description || "",
minHeight: "140px"
});
fields.press = mountMultilingualField(document.getElementById("f-press-wrap"), {
type: "textarea",
value: e?.press || "",
minHeight: "70px"
});
mountImageField(document.getElementById("f-hero-wrap"), {
id: "f-hero",
value: e?.hero || "",
placeholder: t("admin.paste_or_upload")
});
fillArtistOptions(e?.artistIds || []);
openModal();
};
window.del = async function (id) {
if (!confirm(t("admin.confirm.delete_exhibition"))) return;
try {
await Store.remove("exhibitions", id);
toast(t("admin.toast.exhibition_removed"));
render();
} catch (err) { alert(err.message || err); }
};
document.getElementById("new-btn").addEventListener("click", () => window.edit(null));
document.getElementById("form").addEventListener("submit", async (ev) => {
ev.preventDefault();
const id = document.getElementById("f-id").value;
const titleVal = fields.title.get();
const item = {
id: id || slugify(titleVal.en || titleVal.ko || titleVal.ja) || uid("exh"),
title: titleVal,
status: document.getElementById("f-status").value,
venue: document.getElementById("f-venue").value.trim(),
startDate: document.getElementById("f-start").value,
endDate: document.getElementById("f-end").value,
hero: document.getElementById("f-hero").value.trim(),
description: fields.description.get(),
press: fields.press.get(),
artistIds: [...document.getElementById("f-artists").selectedOptions].map((o) => o.value)
};
try {
await Store.upsert("exhibitions", item);
closeModal();
toast(t("admin.toast.saved"));
render();
} catch (err) { alert(err.message || err); }
});
})();
</script>
</body>
</html>