- 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
113 lines
4.6 KiB
HTML
113 lines
4.6 KiB
HTML
<!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>
|