summaryrefslogtreecommitdiff
path: root/static/index.html
blob: 55c99e8661a6df9af28a54a15fc3089b69cffeb1 (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
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>西荻窪周辺 スタジオ空き枠</title>
<style>
  :root { --free: #2f9e5b; --free-bg: #e3f5ea; --booked: #b8b8b8; --line: #ddd; }
  * { box-sizing: border-box; }
  body { font-family: system-ui, "Hiragino Sans", "Noto Sans JP", sans-serif; margin: 0; padding: 16px; color: #222; }
  h1 { font-size: 20px; margin: 0 0 12px; }
  .bar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 12px; }
  .days { display: flex; gap: 4px; overflow-x: auto; }
  .days button { min-width: 64px; }
  button, input, select { font: inherit; padding: 6px 10px; border: 1px solid var(--line); border-radius: 6px; background: #fff; cursor: pointer; }
  button.active { background: #222; color: #fff; border-color: #222; }
  .sat { color: #2463c2; } .sun { color: #c23a3a; }
  button.active.sat, button.active.sun { color: #fff; }
  .wrap { overflow-x: auto; }
  table { border-collapse: collapse; min-width: 100%; }
  th, td { border: 1px solid var(--line); padding: 0; text-align: center; font-size: 13px; }
  thead th { background: #f6f6f6; padding: 6px 4px; position: sticky; top: 0; }
  th.studio { text-align: left; padding: 6px 10px; white-space: nowrap; background: #fafafa; }
  th.studio a { color: inherit; }
  th.studio .meta { display: block; font-weight: normal; font-size: 11px; color: #777; }
  td.room { padding: 6px 8px; white-space: nowrap; font-weight: bold; }
  td.cell { width: 44px; height: 32px; }
  td.free { background: var(--free-bg); color: var(--free); font-weight: bold; }
  td.free a { display: block; line-height: 32px; color: inherit; text-decoration: none; }
  td.booked { background: repeating-linear-gradient(45deg, #f2f2f2, #f2f2f2 4px, #e8e8e8 4px, #e8e8e8 8px); color: var(--booked); }
  td.closed { background: #fff; }
  tr.hidden { display: none; }
  .err { color: #c23a3a; font-size: 12px; }
  .note { font-size: 12px; color: #777; margin-top: 10px; }
</style>
</head>
<body>
<h1>西荻窪周辺 スタジオ空き枠</h1>

<div class="bar">
  <div class="days" id="days"></div>
  <input type="date" id="date">
</div>
<div class="bar">
  <label>時間帯
    <select id="from"></select> 〜 <select id="to"></select>
  </label>
  <label>連続
    <select id="span">
      <option value="1">1時間以上</option>
      <option value="2" selected>2時間以上</option>
      <option value="3">3時間以上</option>
    </select>
  </label>
  <label><input type="checkbox" id="onlyFree" checked> 空きがある部屋だけ</label>
</div>

<div class="wrap"><table id="grid"></table></div>
<p class="note" id="note"></p>

<script>
const $ = (id) => document.getElementById(id);
const WEEK = ["日", "月", "火", "水", "木", "金", "土"];
const ymd = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
let data = null;

for (let h = 0; h <= 24; h++) {
  $("from").add(new Option(`${h}時`, h));
  $("to").add(new Option(`${h}時`, h));
}
$("from").value = 10;
$("to").value = 24;

function renderDays(selected) {
  const today = new Date();
  $("days").innerHTML = "";
  for (let i = 0; i < 14; i++) {
    const d = new Date(today.getFullYear(), today.getMonth(), today.getDate() + i);
    const b = document.createElement("button");
    b.textContent = `${d.getMonth() + 1}/${d.getDate()}(${WEEK[d.getDay()]})`;
    if (d.getDay() === 6) b.classList.add("sat");
    if (d.getDay() === 0) b.classList.add("sun");
    if (ymd(d) === selected) b.classList.add("active");
    b.onclick = () => load(ymd(d));
    $("days").append(b);
  }
}

async function load(day) {
  $("date").value = day;
  renderDays(day);
  const res = await fetch(`/api/slots?date=${day}`);
  data = await res.json();
  render();
}

// 指定した時間帯の中に、span 時間以上続く空きがあるか
function hasFreeRun(byHour, from, to, span) {
  let run = 0;
  for (let h = from; h < to; h++) {
    run = byHour[h] === "free" ? run + 1 : 0;
    if (run >= span) return true;
  }
  return false;
}

function render() {
  const from = +$("from").value, to = +$("to").value, span = +$("span").value;
  const onlyFree = $("onlyFree").checked;
  const hours = [];
  for (let h = from; h < to; h++) hours.push(h);

  const rows = [`<thead><tr><th>スタジオ</th><th>部屋</th>${hours.map((h) => `<th>${h}</th>`).join("")}</tr></thead><tbody>`];
  let shown = 0;

  for (const s of data.studios) {
    const rooms = s.rooms.map((room) => {
      const byHour = {};
      for (const slot of s.slots) if (slot.room === room) byHour[slot.hour] = slot.status;
      return { room, byHour, ok: hasFreeRun(byHour, from, to, span) };
    });
    const visible = rooms.filter((r) => !onlyFree || r.ok);
    if (!visible.length && !s.error) continue;
    shown += visible.length;

    const link = s.booking ? `<a href="${s.booking}" target="_blank" rel="noopener">${s.name}</a>` : `<a href="${s.url}" target="_blank" rel="noopener">${s.name}</a>`;
    const meta = [s.area, s.phone, s.provider === "sample" ? "サンプルデータ" : ""].filter(Boolean).join(" / ");
    const head = `<th class="studio" rowspan="${Math.max(visible.length, 1)}">${link}<span class="meta">${meta}</span>${s.error ? `<span class="err">取得失敗: ${s.error}</span>` : ""}</th>`;

    if (!visible.length) {
      rows.push(`<tr>${head}<td colspan="${hours.length + 1}"></td></tr>`);
      continue;
    }
    visible.forEach((r, i) => {
      const cells = hours.map((h) => {
        const st = r.byHour[h];
        if (st === "free") {
          const href = s.booking || s.url;
          return `<td class="cell free" title="${r.room} ${h}:00-${h + 1}:00 空き"><a href="${href}" target="_blank" rel="noopener">○</a></td>`;
        }
        if (st === "booked") return `<td class="cell booked">×</td>`;
        return `<td class="cell closed"></td>`;
      }).join("");
      rows.push(`<tr>${i === 0 ? head : ""}<td class="room">${r.room}</td>${cells}</tr>`);
    });
  }
  rows.push("</tbody>");
  $("grid").innerHTML = rows.join("");
  $("note").textContent = shown
    ? "○ をクリックすると予約ページ(なければ公式サイト)を開きます。電話予約のみのスタジオは電話番号を表示しています。"
    : "条件に合う空き枠がありません。";
}

["from", "to", "span", "onlyFree"].forEach((id) => $(id).addEventListener("change", () => data && render()));
$("date").addEventListener("change", (e) => e.target.value && load(e.target.value));
load(ymd(new Date()));
</script>
</body>
</html>