diff options
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/ParksTable.astro | 210 |
1 files changed, 210 insertions, 0 deletions
diff --git a/src/components/ParksTable.astro b/src/components/ParksTable.astro new file mode 100644 index 0000000..268bdfe --- /dev/null +++ b/src/components/ParksTable.astro @@ -0,0 +1,210 @@ +--- +// 小平市の公園一覧(/kodaira-data/parks/)を表示する。 +// データは src/data/kodaira-parks.json(321件、市の公開資料から作成)。 +// 町名セレクトとキーワード入力で絞り込む(絞り込みは利用者の端末で実行)。 +import parksData from "@/data/kodaira-parks.json"; + +type Park = Record<string, string>; +const parks = parksData as Park[]; + +const EMPTY = "—"; + +const columns: { key: string; label: string; numeric?: boolean; wide?: boolean }[] = [ + { key: "公園名", label: "公園名" }, + { key: "住所", label: "住所" }, + { key: "面積㎡", label: "面積(㎡)", numeric: true }, + { key: "500㎡以上", label: "500㎡以上" }, + { key: "種別", label: "種別" }, + { key: "南西部指定管理", label: "南西部指定管理" }, + { key: "トイレ", label: "トイレ", wide: true }, + { key: "水飲み場", label: "水飲み場" }, + { key: "ベンチ・椅子", label: "ベンチ・椅子", wide: true }, + { key: "テーブル", label: "テーブル", wide: true }, + { key: "防災用具", label: "防災用具", wide: true }, + { key: "ボール遊び", label: "ボール遊び", wide: true }, + { key: "遊具", label: "遊具", wide: true }, + { key: "備考", label: "備考", wide: true }, +]; + +function display(key: string, value: string): string { + if (value === "" || value == null) return EMPTY; + if (key === "面積㎡") { + const n = Number(value); + if (Number.isFinite(n)) return n.toLocaleString("ja-JP"); + } + return value; +} + +// 検索対象(公園名・住所・町名・種別・ボール遊び・遊具・備考)。 +function searchIndex(p: Park): string { + return [ + p["公園名"], + p["住所"], + p["町名"], + p["種別"], + p["ボール遊び"], + p["遊具"], + p["備考"], + ] + .join(" ") + .toLowerCase(); +} + +const towns = [...new Set(parks.map((p) => p["町名"]))].sort((a, b) => + a.localeCompare(b, "ja"), +); +const total = parks.length; +--- + +<div class="parks" data-parks> + <div class="parks-controls"> + <label> + <span>町名</span> + <select data-parks-town> + <option value="">すべての町名</option> + {towns.map((town) => ( + <option value={town}>{town}</option> + ))} + </select> + </label> + <label> + <span>キーワード</span> + <input type="search" data-parks-search placeholder="公園名・住所・遊具など" /> + </label> + <span class="parks-count" data-parks-count aria-live="polite">{total}件</span> + </div> + + <div class="parks-scroll"> + <table> + <thead> + <tr> + {columns.map((col) => ( + <th scope="col">{col.label}</th> + ))} + </tr> + </thead> + <tbody> + {parks.map((park) => ( + <tr data-town={park["町名"]} data-search={searchIndex(park)}> + {columns.map((col) => ( + <td + class:list={[ + col.numeric && "is-num", + col.wide && "is-wide", + park[col.key] === "" && "is-empty", + ]} + > + {display(col.key, park[col.key])} + </td> + ))} + </tr> + ))} + </tbody> + </table> + </div> +</div> + +<style> + .parks-controls { + display: flex; + flex-wrap: wrap; + gap: 0.75rem 1rem; + align-items: center; + margin: 1rem 0; + } + .parks-controls label { + display: inline-flex; + align-items: center; + gap: 0.4rem; + font-size: 0.9rem; + } + .parks-controls select, + .parks-controls input { + padding: 0.3rem 0.5rem; + border: 1px solid var(--sl-color-hairline, #ccc); + border-radius: 0.3rem; + background: var(--sl-color-bg, #fff); + color: var(--sl-color-text, inherit); + font: inherit; + } + .parks-count { + margin-left: auto; + font-size: 0.9rem; + color: var(--sl-color-gray-3, #666); + } + .parks-scroll { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + border: 1px solid var(--sl-color-hairline, #ddd); + border-radius: 0.3rem; + } + .parks table { + width: 100%; + border-collapse: collapse; + font-size: 0.85rem; + } + .parks th, + .parks td { + border-bottom: 1px solid var(--sl-color-hairline, #ddd); + border-right: 1px solid var(--sl-color-hairline, #ddd); + padding: 0.35rem 0.55rem; + text-align: left; + vertical-align: top; + white-space: nowrap; + } + .parks th:last-child, + .parks td:last-child { + border-right: none; + } + .parks thead th { + position: sticky; + top: 0; + z-index: 1; + background: var(--sl-color-gray-6, #f2f2f2); + color: var(--sl-color-white, inherit); + } + .parks tbody tr:nth-child(even) td { + background: color-mix(in srgb, var(--sl-color-text, #333) 5%, transparent); + } + .parks .is-num { + text-align: right; + font-variant-numeric: tabular-nums; + } + .parks .is-wide { + white-space: normal; + min-width: 11rem; + max-width: 20rem; + } + .parks .is-empty { + color: var(--sl-color-gray-4, #aaa); + text-align: center; + } +</style> + +<script> + document.querySelectorAll("[data-parks]").forEach((root) => { + const select = root.querySelector<HTMLSelectElement>("[data-parks-town]"); + const search = root.querySelector<HTMLInputElement>("[data-parks-search]"); + const count = root.querySelector("[data-parks-count]"); + const rows = Array.from(root.querySelectorAll<HTMLTableRowElement>("tbody tr")); + const total = rows.length; + + const apply = () => { + const town = select ? select.value : ""; + const query = search ? search.value.trim().toLowerCase() : ""; + let shown = 0; + rows.forEach((row) => { + const okTown = !town || row.dataset.town === town; + const okText = !query || (row.dataset.search || "").includes(query); + const ok = okTown && okText; + row.hidden = !ok; + if (ok) shown += 1; + }); + if (count) count.textContent = `${shown} / ${total}件`; + }; + + select?.addEventListener("change", apply); + search?.addEventListener("input", apply); + apply(); + }); +</script> |
