PageSourceSearch

https://kurai-muki.com/assets/js/class-trial-calendar.js?v=f102db1ebe0b

js kurai-muki.com collected 2026-10-06 11:20:25 UTC 13,190 bytes, 314 lines download raw bytes

1(function () {
2  "use strict";
3
4  var calendarEl = document.getElementById("cal");
5  var statusEl = document.getElementById("class-trial-calendar-status");
6  var daysEl = document.getElementById("days");
7  if (!calendarEl || !daysEl || !statusEl) return;
8  var buttons = document.querySelectorAll(".avail-tabs button");
9
10  var GCAL_API_KEY = "AIzaSyBGWH7t0N5npf8vw9CxW30cUoscbAFoKUQ";
11  var GCAL_CALENDAR_ID = "587999a926ec1ae1494fd9ed45ac9b61cea8af6188378d992d4c145d47d29d87@group.calendar.google.com";
12  var POLICY_URL = "./yoyaku/availability-policy.json";
13  var BOOKING_URL = "./yoyaku/";
14  var lessonId = "trial";
15  var WEEKDAY_ABBR = ["日", "月", "火", "水", "木", "金", "土"];
16  var WEEKDAY_FULL = ["日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日"];
17
18  var policy = null;
19  var seatInfoByDate = {};
20  var seatOverlayActive = false;
21  var today = tokyoYMD(new Date());
22  var minISO = addDaysISO(toISO(today), 1);
23  var maxISO = null;
24  var loadFailed = false;
25
26  function pad2(value) {
27    return String(value).padStart(2, "0");
28  }
29
30  function tokyoYMD(date) {
31    var formatter = new Intl.DateTimeFormat("en-CA", {
32      timeZone: "Asia/Tokyo",
33      year: "numeric",
34      month: "2-digit",
35      day: "2-digit",
36    });
37    var parts = {};
38    formatter.formatToParts(date).forEach(function (part) {
39      if (part.type !== "literal") parts[part.type] = part.value;
40    });
41    return {
42      y: parseInt(parts.year, 10),
43      m: parseInt(parts.month, 10),
44      d: parseInt(parts.day, 10),
45    };
46  }
47
48  function toISO(ymd) {
49    return ymd.y + "-" + pad2(ymd.m) + "-" + pad2(ymd.d);
50  }
51
52  function fromISO(iso) {
53    var parts = iso.split("-").map(Number);
54    return { y: parts[0], m: parts[1], d: parts[2] };
55  }
56
57  function addDaysISO(iso, delta) {
58    var ymd = fromISO(iso);
59    var date = new Date(Date.UTC(ymd.y, ymd.m - 1, ymd.d));
60    date.setUTCDate(date.getUTCDate() + delta);
61    return date.getUTCFullYear() + "-" + pad2(date.getUTCMonth() + 1) + "-" + pad2(date.getUTCDate());
62  }
63
64  function addMonthsClamped(ymd, count) {
65    var total = ymd.m - 1 + count;
66    var year = ymd.y + Math.floor(total / 12);
67    var month = ((total % 12) + 12) % 12 + 1;
68    var lastDay = new Date(year, month, 0).getDate();
69    return { y: year, m: month, d: Math.min(ymd.d, lastDay) };
70  }
71
72  function weekdayOfISO(iso) {
73    var ymd = fromISO(iso);
74    return new Date(Date.UTC(ymd.y, ymd.m - 1, ymd.d)).getUTCDay();
75  }
76
77  function evaluatePolicy(dateISO) {
78    var lesson = policy.lessons[lessonId];
79    if (!lesson || dateISO < policy.holidays.coverageFrom || dateISO > policy.holidays.coverageThrough) {
80      return { allowed: false, slots: [], reason: "coverage" };
81    }
82    if (lesson.blockHolidays && Object.prototype.hasOwnProperty.call(policy.holidays.dates, dateISO)) {
83      return { allowed: false, slots: [], reason: "holiday" };
84    }
85    var weekday = weekdayOfISO(dateISO);
86    var rule = lesson.rules.find(function (candidate) {
87      return candidate.weekday === weekday;
88    });
89    if (!rule) return { allowed: false, slots: [], reason: "weekday" };
90    if (rule.saturdayOrdinals && rule.saturdayOrdinals.indexOf(Math.ceil(fromISO(dateISO).d / 7)) === -1) {
91      return { allowed: false, slots: [], reason: "saturday" };
92    }
93    return { allowed: true, slots: rule.slots.slice(), reason: "ok" };
94  }
95
96  function classifyGroupTitle(title) {
97    var text = String(title == null ? "" : title).trim();
98    // penny 8/17裁定: プライベート休講も通常枠には波及させない。
99    if (!text || text.indexOf("プライベート") === 0) return null;
100    if (text.indexOf("休講") !== -1) return { kind: "closed", badge: "休講" };
101    if (text.indexOf("満席") !== -1) return { kind: "full", badge: "満席" };
102    if (text.indexOf("午後のみ") !== -1) return { kind: "pm-only", badge: "午後のみ" };
103    if (text.indexOf("午前のみ") !== -1) return { kind: "am-only", badge: "午前のみ" };
104    var seats = text.match(/空き\s*(\d+)\s*席/);
105    if (seats) return { kind: "open", badge: "空き" + parseInt(seats[1], 10) + "席" };
106    return { kind: "unknown", badge: text };
107  }
108
109  function summarizeDay(titles, privateLesson) {
110    var parsed = titles.map(function (title) {
111      if (!privateLesson) return classifyGroupTitle(title);
112      var text = String(title == null ? "" : title).trim();
113      if (text.indexOf("プライベート") === 0) {
114        if (text.indexOf("休講") !== -1) return { kind: "closed", badge: "休講" };
115        if (text.indexOf("満席") !== -1) return { kind: "full", badge: "満席" };
116        return null;
117      }
118      if (text.indexOf("休講") !== -1) return { kind: "closed", badge: "休講" };
119      return null;
120    }
120).filter(Boolean);
121    var closed = parsed.find(function (item) { return item.kind === "closed"; });
122    if (closed) return closed;
123    if (parsed.length === 0) return null;
124
125    var knownKinds = {};
126    parsed.forEach(function (item) {
127      if (item.kind !== "unknown") knownKinds[item.kind] = true;
128    });
129    if (Object.keys(knownKinds).length > 1) {
130      var badges = [];
131      parsed.forEach(function (item) {
132        if (item.badge && badges.indexOf(item.badge) === -1) badges.push(item.badge);
133      });
134      return { kind: "unknown", badge: badges.join("・") };
135    }
136    return parsed.find(function (item) { return item.kind !== "unknown"; }) || parsed[0];
137  }
138
139  function buildSeatInfo(items) {
140    var titlesByDate = {};
141    (items || []).forEach(function (item) {
142      if (!item || !item.start) return;
143      var startISO = item.start.date || (item.start.dateTime ? String(item.start.dateTime).slice(0, 10) : null);
144      if (!startISO || !/^\d{4}-\d{2}-\d{2}$/.test(startISO)) return;
145      var endISO = item.start.date && item.end && item.end.date ? item.end.date : null;
146      if (!endISO || endISO <= startISO) endISO = addDaysISO(startISO, 1);
147      var guard = 0;
148      for (var dateISO = startISO; dateISO < endISO && guard < 62; dateISO = addDaysISO(dateISO, 1), guard += 1) {
149        (titlesByDate[dateISO] = titlesByDate[dateISO] || []).push(item.summary);
150      }
151    });
152    var result = {};
153    Object.keys(titlesByDate).forEach(function (dateISO) {
154      result[dateISO] = {
155        trial: summarizeDay(titlesByDate[dateISO], false),
156        private: summarizeDay(titlesByDate[dateISO], true),
157      };
158    });
159    return result;
160  }
161
162  function isMorning(slot) {
163    return String(slot).slice(0, 5) < "13:00";
164  }
165
166  function evaluateDay(dateISO) {
167    if (dateISO < minISO || dateISO > maxISO) return { allowed: false, slots: [], reason: "window" };
168    var base = evaluatePolicy(dateISO);
169    if (!base.allowed || !seatOverlayActive) return base;
170    var seat = seatInfoByDate[dateISO] && seatInfoByDate[dateISO][lessonId];
171    if (!seat) return base;
172    if (seat.kind === "closed" || seat.kind === "full") {
173      return { allowed: false, slots: [], reason: seat.kind, badge: seat.badge };
174    }
175    if (seat.kind === "pm-only" || seat.kind === "am-only") {
176      var keepMorning = seat.kind === "am-only";
177      var slots = base.slots.filter(function (slot) { return isMorning(slot) === keepMorning; });
178      return slots.length
179        ? { allowed: true, slots: slots, reason: "ok", badge: seat.badge }
180        : { allowed: false, slots: [], reason: "no-slot", badge: seat.badge };
181    }
182    return { allowed: true, slots: base.slots, reason: "ok", badge: seat.badge };
183  }
184
185  function createDay(dateISO, availability, strip, showMonth) {
186    var ymd = fromISO(dateISO);
187    var dayEl = document.createElement(availability.allowed ? "a" : "span");
188    var scheduled = dateISO >= minISO && dateISO <= maxISO && evaluatePolicy(dateISO).allowed;
189    dayEl.className = strip ? "day" : "c";
190    if (scheduled && !availability.allowed) dayEl.className += " full";
191    if (availability.allowed) {
192      dayEl.href = BOOKING_URL + "?lesson=" + lessonId + "&date=" + dateISO + "#availability-calendar";
193    }
194    var accessibleLabel = ymd.y + "年" + ymd.m + "月" + ymd.d + "日 " + WEEKDAY_FULL[weekdayOfISO(dateISO)];
195    accessibleLabel += availability.allowed ? "、予約可能" : "、予約できません";
196    if (availability.badge) accessibleLabel += "、" + availability.badge;
197    dayEl.setAttribute("aria-label", accessibleLabel);
198    if (dateISO === toISO(today)) dayEl.setAttribute("aria-current", "date");
199    if (!strip && showMonth) {
200      var month = document.createElement("span");
201      month.className = "mo";
202      month.textContent = ymd.m + "月";
203      dayEl.appendChild(month);
204    }
205    var number = document.createElement("span");
206    number.className = "d";
207    number.textContent = strip ? ymd.m + "/" + ymd.d : String(ymd.d);
208    dayEl.appendChild(number);
209    if (strip) {
210      var weekday = document.createElement("span");
211      weekday.className = "w";
212      weekday.textContent = WEEKDAY_ABBR[weekdayOfISO(dateISO)];
213      dayEl.appendChild(weekday);
214    }
215    if (strip || scheduled) {
216      var mark = document.createElement("span");
217      mark.className = "m";
218      mark.setAttribute("aria-hidden", "true");
219      mark.textContent = availability.allowed ? "○" : "×";
220      dayEl.appendChild(mark);
221    }
222    return dayEl;
223  }
224
225  function render() {
226    if (loadFailed) { showError(); return; }
227    if (!seatOverlayActive) return;
228    daysEl.innerHTML = "";
229    calendarEl.innerHTML = "";
230    daysEl.setAttribute("aria-busy", "false");
231    calendarEl.setAttribute("aria-busy", "false");
232    var count = 0;
233    for (var iso = minISO; iso <= maxISO && count < 8; iso = addDaysISO(iso, 1)) {
234      if (!evaluatePolicy(iso).allowed) continue;
235      var stripItem = document.createElement("li");
236      stripItem.appendChild(createDay(iso, evaluateDay(iso), true, false));
237      daysEl.appendChild(stripItem);
238      count += 1;
239    }
240    WEEKDAY_ABBR.forEach(function (weekday) {
241      var heading = document.createElement("li");
242      heading.className = "h";
243      heading.textContent = weekday;
244      calendarEl.appendChild(heading);
245    });
246    var startISO = addDaysISO(toISO(today), -weekdayOfISO(toISO(today)));
247    for (var i = 0; i < 35; i += 1) {
248      var dateISO = addDaysISO(startISO, i);
249      var cell = document.createElement("li");
250      cell.appendChild(createDay(dateISO, evaluateDay(dateISO), false, i === 0 || fromISO(dateISO).d === 1));
251      calendarEl.appendChild(cell);
252    }
253    statusEl.textContent = (lessonId === "private" ? "プライベート" : "体験・通常")
254      + "レッスンの空き状況です。空いている日を選ぶと予約フォームへ進みます。";
255  }
256
257  function showError() {
258    loadFailed = true;
259    [daysEl, calendarEl].forEach(function (element) {
260      element.setAttribute("aria-busy", "false");
261      element.innerHTML = '<li class="class-calendar-loading">現在カレンダーを表示できません。<a href="'
262        + BOOKING_URL + '?lesson=' + lessonId + '">予約フォームでご確認ください。</a><br>お手数ですが、LINE・お電話・予約フォームからお問い合わせください。</li>';
263    });
264    statusEl.textContent = "お手数ですが、LINE・お電話・予約フォームからお問い合わせください。";
265  }
266
267  buttons.forEach(function (button) {
268    button.addEventListener("click", function () {
269      lessonId = button.dataset.kind === "pvt" ? "private" : "trial";
270      buttons.forEach(function (item) {
271        item.setAttribute("aria-pressed", String(item.dataset.kind === button.dataset.kind));
272      });
273      render();
274    });
275  });
276
277  function loadSeatOverlay() {
278    var url = "https://www.googleapis.com/calendar/v3/calendars/"
279      + encodeURIComponent(GCAL_CALENDAR_ID) + "/events"
280      + "?key=" + encodeURIComponent(GCAL_API_KEY)
281      + "&timeMin=" + encodeURIComponent(minISO + "T00:00:00+09:00")
282      + "&timeMax=" + encodeURIComponent(addDaysISO(maxISO, 1) + "T00:00:00+09:00")
283      + "&singleEvents=true&orderBy=startTime&maxResults=250"
284      + "&timeZone=" + encodeURIComponent("Asia/Tokyo")
285      + "&fields=" + encodeURIComponent("items(summary,start,end)");
286    fetch(url)
287      .then(function (response) {
288        if (!response.ok) throw new Error("calendar fetch failed: " + response.status);
289        return response.json();
290      })
291      .then(function (data) {
292        seatInfoByDate = buildSeatInfo(data.items || []);
293        seatOverlayActive = true;
294        render();
295      })
296      .catch(showError);
297  }
298
299  if (!window.fetch) { showError(); return; }
300
301  fetch(POLICY_URL, { credentials: "same-origin" })
302    .then(function (response) {
303      if (!response.ok) throw new Error("policy fetch failed: " + response.status);
304      return response.json();
305    })
306    .then(function (data) {
307      if (!data || !data.lessons || !data.lessons.trial || !data.lessons.private || !data.holidays) throw new Error("invalid policy");
308      policy = data;
309      maxISO = toISO(addMonthsClamped(today, policy.bookingWindowMonths || 3));
310      if (maxISO > policy.holidays.coverageThrough) throw new Error("policy coverage exceeded");
311      loadSeatOverlay();
312    })
313    .catch(showError);
314})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.