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.