aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/components/ParksTable.astro
blob: 268bdfea868946331ba5f736d5b3edf0ba9a84ea (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
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>