aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/components
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-06 13:59:27 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-06 13:59:27 +0900
commit0f3b1475f26f1dbfc857dc785a2c25b2012a6dcb (patch)
treedffec7454ebfaafe28a6a05260f75eafa9554e00 /src/components
parent350c1563abc8ab66aaaed8cfbc6d62bdfeadb0da (diff)
ParksTable: 小平市の公園一覧ページを追加
- /kodaira-data/parks/ に公園321件の一覧表を追加(町名・キーワードで絞り込み可) - サイドバーに「小平市のデータ」グループを追加 - reference/ など調査用素材を .gitignore に追加
Diffstat (limited to 'src/components')
-rw-r--r--src/components/ParksTable.astro210
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>