/* 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(fmtTime(e.start)) + ' – ' + escHtml(fmtTime(e.end)) + '' + '' + escHtml(e.cost || 'Free') + '
' + '

' + escHtml(e.title) + '

' + range + '
' + escHtml(e.venue) + '
' + '

' + 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 = '' + '
Week of ' + escHtml(weekStart.toLocaleDateString('en-US', { month: 'long', day: 'numeric' })) + '' + escHtml(monthLabel) + '
' + '' + ''; container.appendChild(head); var prev = $('[data-nav="-1"]', head), next = $('[data-nav="1"]', head); if (weekStart <= bounds.min) prev.disabled = true; if (addDays(weekStart, 7) > bounds.max) next.disabled = true; $all('.sab-ev-nav', head).forEach(function (btn) { btn.addEventListener('click', function () { render(container, events, addDays(weekStart, 7 * parseInt(btn.getAttribute('data-nav'), 10)), bounds); }); }); $('.sab-ev-today', head).addEventListener('click', function () { var now = startOfWeek(new Date()); if (now < bounds.min) now = bounds.min; if (now > bounds.max) now = bounds.max; render(container, events, now, bounds); }); var list = document.createElement('div'); list.className = 'sab-ev-list'; for (var i = 0; i < 7; i++) { var d = addDays(weekStart, i), ymd = fmtYMD(d); var day = document.createElement('section'); day.className = 'sab-ev-day' + (ymd === todayYmd ? ' sab-ev-today-hl' : '') + (ymd < todayYmd ? ' sab-ev-past' : ''); var dayEvents = eventsOn(events, ymd); day.innerHTML = '

' + escHtml(fmtDay(d)) + (ymd === todayYmd ? ' Today' : '') + '

'; if (dayEvents.length === 0) { var none = document.createElement('p'); none.className = 'sab-ev-none'; none.textContent = 'No free events listed for this day yet — check back, we update weekly.'; day.appendChild(none); } else { dayEvents.forEach(function (e) { day.appendChild(eventCard(e, ymd)); }); } list.appendChild(day); } container.appendChild(list); var foot = document.createElement('p'); foot.className = 'sab-ev-foot'; foot.textContent = 'All events listed here are free. Times are Central. We update this calendar every week — the .ics download includes a 1-hour reminder.'; container.appendChild(foot); } function init() { $all('.sab-events-cal').forEach(function (container) { var url = container.getAttribute('data-json'); if (!url) return; container.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(); } })();