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:
112
admin/dashboard.html
Normal file
112
admin/dashboard.html
Normal 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>
|
||||
Reference in New Issue
Block a user