From 70ce625487b5e0b7540b70de2ae59f0086134a9e Mon Sep 17 00:00:00 2001 From: dax Date: Wed, 5 Aug 2026 17:15:39 +0100 Subject: admin import panel with upload progress + recent photos; app empty-state links to /admin --- public/admin.js | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) (limited to 'public/admin.js') diff --git a/public/admin.js b/public/admin.js index fa74635..0540236 100644 --- a/public/admin.js +++ b/public/admin.js @@ -293,9 +293,82 @@ async function enterDash() { show("dash") try { await loadStatus() + await loadRecent() } catch (err) { if (views.dash.hidden) return; } } +async function loadRecent() { + const data = await api("/api/admin/photos?recent=1&limit=12") + const grid = $("#im-new") + grid.replaceChildren() + for (const p of data.photos) { + const box = photoBox(p) + box.addEventListener("click", () => { + document.querySelectorAll(".tab").forEach((x) => x.classList.remove("active")) + document.querySelector('.tab[data-tab="graph"]').classList.add("active") + $("#tab-status").hidden = true + $("#tab-import").hidden = true + $("#tab-graph").hidden = false + loadGraph(p.id) + }) + grid.appendChild(box) + } + $("#im-recent").hidden = data.photos.length === 0 +} + +function setImProgress(pct, text) { + $("#im-progress").hidden = false + $("#im-status").textContent = text + const fill = $("#im-fill") + if (pct === "indeterminate") fill.classList.add("indeterminate") + else { + fill.classList.remove("indeterminate") + fill.style.width = pct + "%" + } +} + +function uploadImport(form) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest() + xhr.open("POST", "/api/import") + xhr.setRequestHeader("Authorization", "Bearer " + getToken()) + xhr.upload.addEventListener("progress", (e) => { + if (e.lengthComputable) setImProgress(Math.min(100, Math.round((e.loaded / e.total) * 100))) + }) + xhr.upload.addEventListener("load", () => setImProgress("indeterminate", "indexing…")) + xhr.addEventListener("load", () => { + let data = {} + try { data = JSON.parse(xhr.responseText) } catch { /* ignore */ } + if (xhr.status >= 200 && xhr.status < 300) resolve(data) + else reject(new Error(data.error || `import failed (${xhr.status})`)) + }) + xhr.addEventListener("error", () => reject(new Error("upload failed (network error)"))) + xhr.send(form) + }) +} + +async function importFiles(list) { + const files = Array.from(list || []).filter((f) => f.size > 0) + if (!files.length) return + const form = new FormData() + for (const f of files) form.append("photos", f, f.name) + const dz = $("#dz") + dz.classList.add("busy") + $("#im-err").textContent = "" + try { + const data = await uploadImport(form) + toast(`imported ${data.imported} ${data.imported === 1 ? "photo" : "photos"}`) + await loadStatus() + await loadRecent() + } catch (err) { + $("#im-err").textContent = err.message + } finally { + dz.classList.remove("busy") + $("#im-progress").hidden = true + $("#im-files").value = "" + } +} + async function init() { document.querySelectorAll(".tab").forEach((b) => { b.addEventListener("click", () => { @@ -303,7 +376,9 @@ async function init() { b.classList.add("active") const tab = b.dataset.tab $("#tab-status").hidden = tab !== "status" + $("#tab-import").hidden = tab !== "import" $("#tab-graph").hidden = tab !== "graph" + if (tab === "import") loadRecent().catch((e2) => toast(e2.message)) }) }) @@ -365,6 +440,18 @@ async function init() { $("#gq").addEventListener("input", () => debounce(runGraphSearch, 220)) + const dz = $("#dz") + dz.addEventListener("click", () => $("#im-files").click()) + dz.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); $("#im-files").click() } }) + $("#im-files").addEventListener("change", () => importFiles($("#im-files").files)) + dz.addEventListener("dragover", (e) => { e.preventDefault(); dz.classList.add("drag") }) + dz.addEventListener("dragleave", () => dz.classList.remove("drag")) + dz.addEventListener("drop", (e) => { + e.preventDefault() + dz.classList.remove("drag") + importFiles(e.dataTransfer.files) + }) + const token = getToken() if (token) { try { -- cgit v1.3.1