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

176 lines
6.5 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>News — 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-date"></label>
<input id="f-date" type="date" required />
</div>
<div class="full">
<label id="lbl-image"></label>
<div id="f-image-wrap"></div>
</div>
<div class="full">
<label id="lbl-excerpt"></label>
<div id="f-excerpt-wrap"></div>
</div>
<div class="full">
<label id="lbl-body"></label>
<div id="f-body-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("news");
document.getElementById("pg-title").textContent = t("admin.page.news");
document.getElementById("pg-sub").textContent = t("admin.page.news_sub");
document.getElementById("new-btn").textContent = t("admin.btn.new_post");
document.getElementById("lbl-title").textContent = t("admin.form.title");
document.getElementById("lbl-date").textContent = t("admin.form.date");
document.getElementById("lbl-image").textContent = t("admin.form.image");
document.getElementById("lbl-excerpt").textContent = t("admin.form.excerpt");
document.getElementById("lbl-body").textContent = t("admin.form.body");
document.getElementById("btn-cancel").textContent = t("admin.btn.cancel");
document.getElementById("btn-save").textContent = t("admin.btn.save");
let fields = {};
function render() {
const rows = [...Store.news()]
.sort((a, b) => b.date.localeCompare(a.date))
.map(
(n) => `
<tr>
<td><img src="${n.image}" alt=""></td>
<td><strong>${L(n.title)}</strong><br><span style="color:var(--muted);font-size:12px">${L(n.excerpt) || ""}</span></td>
<td>${fmtDateShort(n.date)}</td>
<td class="actions">
<button class="btn ghost btn-small" onclick="edit('${n.id}')">${t("admin.btn.edit")}</button>
<button class="btn danger btn-small" onclick="del('${n.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.date")}</th><th></th></tr></thead>
<tbody>${rows || `<tr><td colspan="4" style="text-align:center;padding:24px;color:var(--muted)">${t("admin.empty.news")}</td></tr>`}</tbody>
`;
}
render();
window.edit = function (id) {
const n = id ? Store.newsItem(id) : null;
document.getElementById("modal-title").textContent = n
? t("admin.modal.edit_post")
: t("admin.modal.new_post");
document.getElementById("f-id").value = n?.id || "";
document.getElementById("f-date").value = n?.date || new Date().toISOString().slice(0, 10);
fields.title = mountMultilingualField(document.getElementById("f-title-wrap"), {
type: "input",
value: n?.title || ""
});
fields.excerpt = mountMultilingualField(document.getElementById("f-excerpt-wrap"), {
type: "textarea",
value: n?.excerpt || "",
minHeight: "70px"
});
fields.body = mountMultilingualField(document.getElementById("f-body-wrap"), {
type: "textarea",
value: n?.body || "",
minHeight: "140px"
});
mountImageField(document.getElementById("f-image-wrap"), {
id: "f-image",
value: n?.image || "",
placeholder: t("admin.paste_or_upload")
});
openModal();
};
window.del = async function (id) {
if (!confirm(t("admin.confirm.delete_post"))) return;
try {
await Store.remove("news", id);
toast(t("admin.toast.post_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("news"),
title: titleVal,
date: document.getElementById("f-date").value,
image: document.getElementById("f-image").value.trim(),
excerpt: fields.excerpt.get(),
body: fields.body.get()
};
try {
await Store.upsert("news", item);
closeModal();
toast(t("admin.toast.saved"));
render();
} catch (err) { alert(err.message || err); }
});
})();
</script>
</body>
</html>