diff options
Diffstat (limited to 'scripts/park-editor.html')
| -rw-r--r-- | scripts/park-editor.html | 148 |
1 files changed, 148 insertions, 0 deletions
diff --git a/scripts/park-editor.html b/scripts/park-editor.html new file mode 100644 index 0000000..01df13f --- /dev/null +++ b/scripts/park-editor.html @@ -0,0 +1,148 @@ +<!doctype html> +<html lang="ja"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>公園データ編集</title> +<style> + :root { color-scheme: light dark; } + body { font-family: system-ui, -apple-system, "Hiragino Kaku Gothic ProN", sans-serif; margin: 0; } + header { position: sticky; top: 0; background: Canvas; border-bottom: 1px solid #9995; padding: .5rem .75rem; display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; z-index: 10; } + header h1 { font-size: 1rem; margin: 0 .5rem 0 0; } + #q { padding: .3rem .5rem; } + #count { font-size: .85rem; opacity: .7; } + #save { padding: .35rem .9rem; font-weight: bold; } + #status { font-size: .85rem; } + .wrap { overflow: auto; height: calc(100vh - 3.2rem); } + table { border-collapse: collapse; font-size: .8rem; } + th, td { border: 1px solid #9994; padding: 0; } + th { position: sticky; top: 0; background: Canvas; padding: .25rem .4rem; white-space: nowrap; z-index: 5; } + td { padding: 0; } + td input { width: 100%; box-sizing: border-box; border: none; padding: .25rem .35rem; font: inherit; background: transparent; color: inherit; } + td input:focus { outline: 2px solid #3b82f6; } + tr.dirty td { background: #ff9d0033; } + tr[hidden] { display: none; } + td.rownum { padding: 0 .4rem; text-align: right; opacity: .5; } + td.mapcell { text-align: center; } + td.mapcell a { text-decoration: none; padding: 0 .4rem; cursor: pointer; } +</style> +</head> +<body> +<header> + <h1>公園データ編集</h1> + <input id="q" placeholder="絞り込み(公園名・町名・住所)" size="24"> + <span id="count"></span> + <button id="save">保存</button> + <button id="revert">再読込</button> + <span id="status"></span> +</header> +<div class="wrap"><table id="tbl"><thead></thead><tbody></tbody></table></div> +<script> +const FIELDS = ["公園名","住所","町名","面積㎡","500㎡以上","種別","南西部指定管理","トイレ","水飲み場","ベンチ・椅子","テーブル","防災用具","ボール遊び","遊具","備考"]; +let parks = []; +const tbl = document.getElementById("tbl"); +const thead = tbl.querySelector("thead"); +const tbody = tbl.querySelector("tbody"); +const q = document.getElementById("q"); +const count = document.getElementById("count"); +const status = document.getElementById("status"); + +function mapsUrl(name) { + return "https://www.google.com/maps/search/?api=1&query=" + encodeURIComponent("小平市" + (name || "")); +} + +async function load() { + const res = await fetch("/api/parks", { cache: "no-store" }); + parks = await res.json(); + render(); +} + +function render() { + thead.innerHTML = "<tr>" + ["#", "地図"].concat(FIELDS).map(function (f) { return "<th>" + f + "</th>"; }).join("") + "</tr>"; + tbody.innerHTML = ""; + parks.forEach(function (p, r) { + const tr = document.createElement("tr"); + const th = document.createElement("td"); + th.className = "rownum"; + th.textContent = String(r + 1); + tr.appendChild(th); + const mapTd = document.createElement("td"); + mapTd.className = "mapcell"; + const mapA = document.createElement("a"); + mapA.textContent = "🗺"; + mapA.title = "Googleマップで開く"; + mapA.href = mapsUrl(p["公園名"] || ""); + mapA.target = "_blank"; + mapA.rel = "noopener"; + mapA.addEventListener("click", function (ev) { + ev.preventDefault(); + const name = tr.querySelector('input[data-c="0"]').value.trim(); + if (name) window.open(mapsUrl(name), "_blank", "noopener"); + }); + mapTd.appendChild(mapA); + tr.appendChild(mapTd); + FIELDS.forEach(function (f, c) { + const td = document.createElement("td"); + const inp = document.createElement("input"); + inp.value = p[f] != null ? p[f] : ""; + inp.dataset.r = r; + inp.dataset.c = c; + inp.addEventListener("input", function () { + tr.classList.add("dirty"); + status.textContent = "未保存の変更があります"; + }); + td.appendChild(inp); + tr.appendChild(td); + }); + tr.dataset.search = (p["公園名"] || "") + " " + (p["町名"] || "") + " " + (p["住所"] || ""); + tbody.appendChild(tr); + }); + filter(); +} + +function filter() { + const s = q.value.trim(); + let shown = 0; + Array.prototype.forEach.call(tbody.children, function (tr) { + const ok = !s || tr.dataset.search.indexOf(s) >= 0; + tr.hidden = !ok; + if (ok) shown++; + }); + count.textContent = shown + " / " + parks.length + " 件"; +} + +function collect() { + Array.prototype.forEach.call(tbody.querySelectorAll("input"), function (inp) { + parks[Number(inp.dataset.r)][FIELDS[Number(inp.dataset.c)]] = inp.value; + }); +} + +document.getElementById("save").onclick = async function () { + collect(); + status.textContent = "保存中…"; + try { + const res = await fetch("/api/parks", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(parks), + }); + if (res.ok) { + status.textContent = "保存しました(" + parks.length + "件)。git にコミットを忘れずに。"; + Array.prototype.forEach.call(tbody.children, function (tr) { tr.classList.remove("dirty"); }); + } else { + status.textContent = "保存に失敗: " + (await res.text()); + } + } catch (e) { + status.textContent = "保存に失敗: " + e.message; + } +}; + +document.getElementById("revert").onclick = function () { + if (confirm("保存していない変更は破棄されます。よろしいですか?")) load(); +}; + +q.addEventListener("input", filter); +load(); +</script> +</body> +</html> |
