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

240 lines
9.4 KiB
HTML
Raw Permalink 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>Artists — 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>
<label id="lbl-name"></label>
<input id="f-name" required />
</div>
<div>
<label id="lbl-born"></label>
<div id="f-born-wrap"></div>
</div>
<div class="full">
<label id="lbl-lives"></label>
<div id="f-lives-wrap"></div>
</div>
<div class="full">
<label id="lbl-portrait"></label>
<div id="f-portrait-wrap"></div>
</div>
<div class="full">
<label id="lbl-bio"></label>
<div id="f-bio-wrap"></div>
</div>
<div class="full">
<label style="display:flex;justify-content:space-between;align-items:center">
<span id="lbl-works"></span>
<button type="button" class="btn ghost btn-small" id="add-work"></button>
</label>
<div class="works-list" id="works"></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("artists");
document.getElementById("pg-title").textContent = t("admin.page.artists");
document.getElementById("pg-sub").textContent = t("admin.page.artists_sub");
document.getElementById("new-btn").textContent = t("admin.btn.new_artist");
document.getElementById("lbl-name").textContent = t("admin.form.name");
document.getElementById("lbl-born").textContent = t("admin.form.born");
document.getElementById("lbl-lives").textContent = t("admin.form.lives");
document.getElementById("lbl-portrait").textContent = t("admin.form.portrait");
document.getElementById("lbl-bio").textContent = t("admin.form.biography");
document.getElementById("lbl-works").textContent = t("admin.form.selected_works");
document.getElementById("add-work").textContent = t("admin.btn.add_work");
document.getElementById("btn-cancel").textContent = t("admin.btn.cancel");
document.getElementById("btn-save").textContent = t("admin.btn.save");
// per-form field handles (for multilingual fields)
let fields = {};
function render() {
const rows = Store.artists()
.map(
(a) => `
<tr>
<td><img src="${a.portrait}" alt=""></td>
<td><strong>${a.name}</strong></td>
<td>${L(a.born) || ""}</td>
<td>${t("admin.works_count", { n: (a.works || []).length })}</td>
<td class="actions">
<a class="btn ghost btn-small" href="../artist.html?id=${a.id}" target="_blank">${t("admin.btn.view")}</a>
<button class="btn ghost btn-small" onclick="edit('${a.id}')">${t("admin.btn.edit")}</button>
<button class="btn danger btn-small" onclick="del('${a.id}')">${t("admin.btn.delete")}</button>
</td>
</tr>
`
)
.join("");
document.getElementById("tbl").innerHTML = `
<thead><tr><th></th><th>${t("admin.table.name")}</th><th>${t("admin.table.born")}</th><th>${t("admin.table.works")}</th><th></th></tr></thead>
<tbody>${rows || `<tr><td colspan="5" style="text-align:center;padding:24px;color:var(--muted)">${t("admin.empty.artists")}</td></tr>`}</tbody>
`;
}
render();
function workRow(w = {}) {
const div = document.createElement("div");
div.className = "work-row";
div.innerHTML = `
<div class="work-row-image"></div>
<div class="work-row-meta">
<div class="ml-field-slot title-slot"></div>
<input placeholder="${t("admin.form.year")}" value="${escapeAttr(w.year || "")}" data-k="year" />
<div class="ml-field-slot medium-slot"></div>
<input placeholder="${t("admin.form.dimensions")}" value="${escapeAttr(w.dimensions || "")}" data-k="dimensions" />
<button type="button" class="btn danger btn-small work-remove" title="Remove">×</button>
</div>
`;
mountImageField(div.querySelector(".work-row-image"), {
value: w.image || "",
dataK: "image",
compact: true,
placeholder: t("admin.paste_or_upload")
});
const titleField = mountMultilingualField(div.querySelector(".title-slot"), {
type: "input",
value: w.title || "",
placeholder: t("admin.form.title")
});
const mediumField = mountMultilingualField(div.querySelector(".medium-slot"), {
type: "input",
value: w.medium || "",
placeholder: t("admin.form.medium")
});
div._titleField = titleField;
div._mediumField = mediumField;
div.querySelector(".work-remove").addEventListener("click", () => div.remove());
return div;
}
window.edit = function (id) {
const a = id ? Store.artist(id) : null;
document.getElementById("modal-title").textContent = a
? t("admin.modal.edit_artist")
: t("admin.modal.new_artist");
document.getElementById("f-id").value = a?.id || "";
document.getElementById("f-name").value = a?.name || "";
fields.born = mountMultilingualField(document.getElementById("f-born-wrap"), {
type: "input",
value: a?.born || "",
placeholder: "b. 1978, Beirut"
});
fields.lives = mountMultilingualField(document.getElementById("f-lives-wrap"), {
type: "input",
value: a?.lives || ""
});
fields.bio = mountMultilingualField(document.getElementById("f-bio-wrap"), {
type: "textarea",
value: a?.bio || ""
});
mountImageField(document.getElementById("f-portrait-wrap"), {
id: "f-portrait",
value: a?.portrait || "",
placeholder: t("admin.paste_or_upload")
});
const wrap = document.getElementById("works");
wrap.innerHTML = "";
(a?.works || []).forEach((w) => wrap.appendChild(workRow(w)));
openModal();
};
window.del = async function (id) {
if (!confirm(t("admin.confirm.delete_artist"))) return;
try {
await Store.remove("artists", id);
toast(t("admin.toast.artist_removed"));
render();
} catch (err) { alert(err.message || err); }
};
document.getElementById("new-btn").addEventListener("click", () => window.edit(null));
document.getElementById("add-work").addEventListener("click", () => {
document.getElementById("works").appendChild(workRow());
});
document.getElementById("form").addEventListener("submit", async (ev) => {
ev.preventDefault();
const id = document.getElementById("f-id").value;
const name = document.getElementById("f-name").value.trim();
const works = [...document.querySelectorAll(".work-row")]
.map((row) => {
const o = {};
row
.querySelectorAll("input[data-k]")
.forEach((i) => (o[i.dataset.k] = i.value.trim()));
if (row._titleField) o.title = row._titleField.get();
if (row._mediumField) o.medium = row._mediumField.get();
return o;
})
.filter((w) => (typeof w.title === "object" ? L(w.title) : w.title) || w.image);
const artist = {
id: id || slugify(name) || uid("artist"),
name,
born: fields.born.get(),
lives: fields.lives.get(),
portrait: document.getElementById("f-portrait").value.trim(),
bio: fields.bio.get(),
works
};
try {
await Store.upsert("artists", artist);
closeModal();
toast(t("admin.toast.saved"));
render();
} catch (err) { alert(err.message || err); }
});
})();
</script>
</body>
</html>