diff --git a/static/js/upcoming.js b/static/js/upcoming.js index dde5f1f..64daf96 100644 --- a/static/js/upcoming.js +++ b/static/js/upcoming.js @@ -64,6 +64,26 @@ function exceptionsByUid(components) { return exceptions; } +/** + * When an occurrence starts, as a point in time. + * + * A date has no time and no zone, its digits are the day itself. toJSDate() + * reads them as midnight in the zone of the browser, which moves an all day + * event by the offset that zone has to Berlin and, far enough east or west, + * onto the day before or after. Keep the digits and read them as UTC instead, + * the table prints an all day event in UTC as well. + * + * @param {ICAL.Time} time Start of the occurrence + * @returns {Date} The point in time to sort and print by + */ +function startOf(time) { + if (time.isDate) { + return new Date(Date.UTC(time.year, time.month - 1, time.day)); + } + + return time.toJSDate(); +} + /** * How far the recurrences of an event have to be iterated. * @@ -98,7 +118,7 @@ function iterationEnd(event, end) { * @param {Date} now Events must still be running at this date * @param {number} maxEvents Maximum number of events to return * @param {number} maxDays Maximum number of days into the future - * @returns {{start: Date, name: string, url: string}[]} url is empty when the event has no URL + * @returns {{start: Date, allDay: boolean, name: string, url: string}[]} url is empty when the event has no URL */ function getUpcomingEvents(icsText, now, maxEvents, maxDays) { const jcal = ICAL.parse(icsText); @@ -153,22 +173,20 @@ function getUpcomingEvents(icsText, now, maxEvents, maxDays) { // judge it by the time it really takes place at. if (details.startDate.toJSDate() <= end && details.endDate.toJSDate() > now) { events.push({ - start: details.startDate.toJSDate(), + start: startOf(details.startDate), + allDay: details.startDate.isDate, name: details.item.summary ?? "", url: eventUrl(details.item), }); } } - } else { - const start = event.startDate.toJSDate(); - - if (start <= end && event.endDate.toJSDate() > now) { - events.push({ - start, - name: event.summary ?? "", - url: eventUrl(event), - }); - } + } else if (event.startDate.toJSDate() <= end && event.endDate.toJSDate() > now) { + events.push({ + start: startOf(event.startDate), + allDay: event.startDate.isDate, + name: event.summary ?? "", + url: eventUrl(event), + }); } } @@ -195,15 +213,21 @@ document.addEventListener("DOMContentLoaded", () => { const colBegin = document.createElement("td"); + // The events take place in Berlin, so name their time in Berlin time + // instead of in the time zone the visitor happens to be in. An all day + // event has no time of day and carries its date in UTC, see startOf(). + const whenFormat = event.allDay + ? { timeZone: "UTC" } + : { timeZone: "Europe/Berlin", hour: "2-digit", minute: "2-digit" }; + const formattedStart = event.start.toLocaleString("de-DE", { weekday: "long", day: "2-digit", month: "2-digit", - hour: "2-digit", - minute: "2-digit", - }).replace(",", ""); + ...whenFormat, + }); - colBegin.innerText = `${formattedStart.replace(" ", ", ")} Uhr`; + colBegin.innerText = event.allDay ? formattedStart : `${formattedStart} Uhr`; row.appendChild(colBegin); const colName = document.createElement("td");