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:
2026-04-25 12:47:36 +09:00
commit 098b55e3b0
32 changed files with 5334 additions and 0 deletions

112
admin/dashboard.html Normal file
View File

@ -0,0 +1,112 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Dashboard — 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>
<a class="btn" href="exhibitions.html" id="new-exh"></a>
</div>
<div class="stats" id="stats"></div>
<div class="panel">
<div class="panel-head">
<h2 id="panel-upc"></h2>
<a class="btn ghost btn-small" href="exhibitions.html" id="manage-exh"></a>
</div>
<table class="table" id="current-tbl"></table>
</div>
<div class="panel">
<div class="panel-head">
<h2 id="panel-news"></h2>
<a class="btn ghost btn-small" href="news.html" id="manage-news"></a>
</div>
<table class="table" id="news-tbl"></table>
</div>
</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("dashboard");
document.getElementById("pg-title").textContent = t("admin.page.dashboard");
document.getElementById("pg-sub").textContent = t("admin.page.dashboard_sub");
document.getElementById("new-exh").textContent = t("admin.btn.new_exhibition");
document.getElementById("panel-upc").textContent = t("admin.dash.current_upcoming");
document.getElementById("manage-exh").textContent = t("admin.btn.manage");
document.getElementById("panel-news").textContent = t("admin.dash.recent_news");
document.getElementById("manage-news").textContent = t("admin.btn.manage");
const a = Store.artists();
const e = Store.exhibitions();
const n = Store.news();
document.getElementById("stats").innerHTML = `
<div class="stat"><div class="k">${t("admin.stat.artists")}</div><div class="v">${a.length}</div></div>
<div class="stat"><div class="k">${t("admin.stat.exhibitions")}</div><div class="v">${e.length}</div></div>
<div class="stat"><div class="k">${t("admin.stat.current_upcoming")}</div><div class="v">${e.filter((x) => x.status !== "past").length}</div></div>
<div class="stat"><div class="k">${t("admin.stat.news")}</div><div class="v">${n.length}</div></div>
`;
const upc = e.filter((x) => x.status !== "past").sort((x, y) => x.startDate.localeCompare(y.startDate));
document.getElementById("current-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></tr></thead>
<tbody>
${
upc.length
? upc
.map((x) => {
const artists = x.artistIds.map((id) => Store.artist(id)?.name).filter(Boolean).join(", ");
return `<tr>
<td><img src="${x.hero}" alt=""></td>
<td><em>${L(x.title)}</em></td>
<td>${artists}</td>
<td>${fmtDateShort(x.startDate)} ${fmtDateShort(x.endDate)}</td>
<td><span class="badge badge-${x.status}">${t("status." + x.status)}</span></td>
</tr>`;
})
.join("")
: `<tr><td colspan="5" style="text-align:center;color:var(--muted);padding:24px">${t("admin.dash.nothing_scheduled")}</td></tr>`
}
</tbody>
`;
const recent = [...n].sort((x, y) => y.date.localeCompare(x.date)).slice(0, 5);
document.getElementById("news-tbl").innerHTML = `
<thead><tr><th></th><th>${t("admin.table.title")}</th><th>${t("admin.table.date")}</th></tr></thead>
<tbody>
${recent
.map(
(x) => `<tr>
<td><img src="${x.image}" alt=""></td>
<td>${L(x.title)}</td>
<td>${fmtDateShort(x.date)}</td>
</tr>`
)
.join("")}
</tbody>
`;
})();
</script>
</body>
</html>