/* SAB Events Calendar — week view with Add to Google Calendar + .ics download. * Events data lives in a separate JSON file (data-json attribute on the container) * so it can be refreshed weekly without touching this script. */ (function () { 'use strict'; var CTZ = 'America/Chicago'; function $(sel, el) { return (el || document).querySelector(sel); } function $all(sel, el) { return Array.prototype.slice.call((el || document).querySelectorAll(sel)); } function pad(n) { return (n < 10 ? '0' : '') + n; } function fmtYMD(d) { return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); } function parseYMD(s) { var p = s.split('-'); return new Date(parseInt(p[0], 10), parseInt(p[1], 10) - 1, parseInt(p[2], 10)); } function startOfWeek(d) { var c = new Date(d.getFullYear(), d.getMonth(), d.getDate()); c.setDate(c.getDate() - c.getDay()); // Sunday return c; } function addDays(d, n) { var c = new Date(d.getFullYear(), d.getMonth(), d.getDate()); c.setDate(c.getDate() + n); return c; } function fmtDay(d) { return d.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' }); } function fmtTime(hhmm) { var p = hhmm.split(':'), h = parseInt(p[0], 10), m = p[1]; var ap = h >= 12 ? 'PM' : 'AM'; h = h % 12; if (h === 0) h = 12; return h + ':' + m + ' ' + ap; } function gcalDate(ymd, hhmm) { return ymd.replace(/-/g, '') + 'T' + hhmm.replace(':', '') + '00'; } function escHtml(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function escIcs(s) { return String(s).replace(/\\/g, '\\\\').replace(/;/g, '\\;') .replace(/,/g, '\\,').replace(/\n/g, '\\n'); } // Events active on a given day (supports multi-day ranges via endDate). function eventsOn(events, ymd) { return events.filter(function (e) { var end = e.endDate || e.date; return e.date <= ymd && ymd <= end; }); } function googleCalUrl(e, ymd) { var params = [ 'action=TEMPLATE', 'text=' + encodeURIComponent(e.title), 'dates=' + gcalDate(ymd, e.start) + '/' + gcalDate(ymd, e.end), 'details=' + encodeURIComponent(e.desc + (e.source ? '\n\nMore info: ' + e.source : '')), 'location=' + encodeURIComponent(e.venue + ', ' + e.address), 'ctz=' + encodeURIComponent(CTZ) ]; return 'https://calendar.google.com/calendar/render?' + params.join('&'); } function icsFor(e, ymd) { var uid = e.id + '-' + ymd + '@sanantoniobenefits.com'; var stamp = new Date().toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'; var lines = [ 'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//SanAntonioBenefits//Free Events Calendar//EN', 'BEGIN:VEVENT', 'UID:' + uid, 'DTSTAMP:' + stamp, 'DTSTART;TZID=' + CTZ + ':' + gcalDate(ymd, e.start), 'DTEND;TZID=' + CTZ + ':' + gcalDate(ymd, e.end), 'SUMMARY:' + escIcs(e.title), 'DESCRIPTION:' + escIcs(e.desc + (e.source ? '\\n\\nMore info: ' + e.source : '')), 'LOCATION:' + escIcs(e.venue + ', ' + e.address), 'BEGIN:VALARM', 'TRIGGER:-PT1H', 'ACTION:DISPLAY', 'DESCRIPTION:' + escIcs('Reminder: ' + e.title), 'END:VALARM', 'END:VEVENT', 'END:VCALENDAR' ]; return lines.join('\r\n'); } function downloadIcs(e, ymd) { var blob = new Blob([icsFor(e, ymd)], { type: 'text/calendar;charset=utf-8' }); var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = e.id + '-' + ymd + '.ics'; document.body.appendChild(a); a.click(); setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000); } function eventCard(e, ymd) { var card = document.createElement('article'); card.className = 'sab-ev-card'; var range = (e.endDate && e.endDate !== e.date) ? '' + escHtml(fmtDay(parseYMD(e.date))) + ' – ' + escHtml(fmtDay(parseYMD(e.endDate))) + '' : ''; card.innerHTML = '
' + escHtml(e.desc) + '
' + ''; var actions = $('.sab-ev-actions', card); if (e.source) { var info = document.createElement('a'); info.href = e.source; info.target = '_blank'; info.rel = 'noopener'; info.className = 'sab-ev-btn sab-ev-info'; info.textContent = 'More info'; actions.appendChild(info); } var g = document.createElement('a'); g.href = googleCalUrl(e, ymd); g.target = '_blank'; g.rel = 'noopener'; g.className = 'sab-ev-btn sab-ev-gcal'; g.textContent = 'Add to Google Calendar'; actions.appendChild(g); var ics = document.createElement('button'); ics.type = 'button'; ics.className = 'sab-ev-btn sab-ev-ics'; ics.textContent = 'Add to Apple / Outlook (.ics)'; ics.setAttribute('aria-label', 'Download calendar file with reminder for ' + e.title); ics.addEventListener('click', function () { downloadIcs(e, ymd); }); actions.appendChild(ics); return card; } function render(container, events, weekStart, bounds) { container.innerHTML = ''; var todayYmd = fmtYMD(new Date()); var head = document.createElement('div'); head.className = 'sab-ev-head'; var monthLabel = weekStart.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); head.innerHTML = '' + 'Loading this week\u2019s free events…
'; fetch(url).then(function (r) { if (!r.ok) throw new Error('fetch failed'); return r.json(); }).then(function (data) { var events = (data.events || []).slice().sort(function (a, b) { return a.date < b.date ? -1 : (a.date > b.date ? 1 : (a.start < b.start ? -1 : 1)); }); if (!events.length) { container.innerHTML = 'No events listed right now — check back soon.
'; return; } var min = startOfWeek(parseYMD(events[0].date)); var lastEnd = events.reduce(function (m, e) { var ed = e.endDate || e.date; return ed > m ? ed : m; }, events[0].date); var max = startOfWeek(parseYMD(lastEnd)); var now = startOfWeek(new Date()); if (now < min) now = min; if (now > max) now = max; render(container, events, now, { min: min, max: max }); }).catch(function () { container.innerHTML = 'Could not load the events calendar right now. Please refresh the page.
'; }); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();