const MAX_COUNTDOWN = 60; let allEventsFromOfficialSchedule = null; var nextId = 0; var countdown = MAX_COUNTDOWN; var now = Date.now(); var eventsOnNow = []; var eventsOnNext = []; var intervalId = null; grist.ready({ requiredAccess: 'read table' }); grist.onRecords(function (records) { eventsOnNow = []; eventsOnNext = []; now = Date.now(); getCampScheduleAndAddToLists(); for (const r of records) { const venue = r["venue"]; if (venueUnderInterest(venue)) { let e = new Event( nextId++, r["title"], venue, r["video_privacy"], r["Record_override"], r["Comments"], Date.parse(r["start_date"]), Date.parse(r["end_date"]) ); addEventToAppropriateList(e); } } sortEventLists(); populateListHtml("happeningNow", eventsOnNow, false); populateListHtml("happeningNext", eventsOnNext, true); if (intervalId != null) { window.clearInterval(intervalId); } intervalId = window.setInterval(() => handleCountdown(), 1000); }); grist.onOptions(function(options, interaction) { console.info("Options reloaded"); }); class Event { constructor(id, name, venue, privacy, recordOverride, comments, startTime, endTime) { this.id = id; this.name = name; this.venue = venue; this.privacy = privacy; this.recordOverride = recordOverride; this.comments = comments; this.startTime = startTime; this.endTime = endTime; } hasStarted() { return this.startTime < now; } hasEnded() { return this.endTime < now; } timeUntilStart() { return new Date(this.startTime - now); } timeUntilEnd() { return new Date(this.endTime - now); } } function makeTextBold(text) { return "" + text + ""; } function convertTimeToString(time) { // Using getHours() is bullshit for this usage, it won't include any days in the hours let hours = Math.floor(time / (1000 * 60 * 60)); let minutes = Math.ceil((time - (hours * 60 * 60 * 1000)) / (1000 * 60)); return hours + " hours, " + minutes + " minutes"; } function getCampScheduleAndAddToLists() { // Get stuff from official schedule and async add to list const apiUrl = "https://www.emfcamp.org/schedule/2026.json"; // Make a GET request fetch(apiUrl) .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(data => { for (const e of data) { if (!((eventsOnNow.concat(eventsOnNext)).map((x) => x.title).includes(e.title))) { for (const o of e.occurrences) { if (venueUnderInterest(o.venue)) { const event = new Event( nextId++, e.title, o.venue, e.video_privacy, false, "", new Date(o.start_time), new Date(o.end_time) ); addEventToAppropriateList(event); sortEventLists(); } } } } }) .catch(error => { console.error('Error:', error); }); } function addEventToAppropriateList(event) { if (event.hasStarted() && !event.hasEnded()) { eventsOnNow.push(event); } else if (!event.hasStarted() && !event.hasEnded()) { eventsOnNext.push(event); } } function venueUnderInterest(venue) { return (["Stage A", "Stage B", "Stage C", "Arts", "Arcade Workshop"].includes(venue)); } function sortEventLists() { eventsOnNow.sort((e1, e2) => { return (e1.timeUntilEnd() > e2.timeUntilEnd()) ? 1 : ((e2.timeUntilEnd() > e1.timeUntilEnd()) ? -1 : 0) }); eventsOnNext.sort((e1, e2) => { return (e1.timeUntilStart() > e2.timeUntilStart()) ? 1 : ((e2.timeUntilStart() > e1.timeUntilStart()) ? -1 : 0)}); } function populateListHtml(elementName, items, getStartsIn) { document.getElementById(elementName).innerHTML = ""; for (const i of items) { var listItem = document.createElement("li"); listItem.setAttribute("class", "list-group-item shadow-sm d-flex flex-wrap align-items-center m-2 rounded-3") listItem.setAttribute("id", i.id); // Basic info of event name, venue and time var title = document.createElement("h4"); title.setAttribute("class", "me-auto") title.innerHTML = i.name; var venue = document.createElement("small"); venue.setAttribute("class", "text-body-secondary"); venue.innerHTML = " " + i.venue + " | "; var time = document.createElement("small"); time.setAttribute("id", i.id + "_time"); time.setAttribute("class", "text-body-secondary"); if (getStartsIn) { time.innerHTML = convertTimeToString(i.timeUntilStart()); } else { time.innerHTML = "(ends in) " + convertTimeToString(i.timeUntilEnd()); } // Red border if show starts in 15 minutes if (getStartsIn) { if (i.timeUntilStart() <= 15 * 60 * 1000){ listItem.setAttribute("class", listItem.getAttribute("class") + " border-2 border-danger"); time.setAttribute("class", "text-danger"); } } title.appendChild(venue); title.appendChild(time); listItem.appendChild(title); // Badges var privacyBadge = document.createElement("span"); if (i.privacy == "none") { privacyBadge.innerHTML = "Do not record" privacyBadge.setAttribute("class", "badge text-bg-danger rounded-pill") } else if (i.privacy == "review") { privacyBadge.innerHTML = "Review" privacyBadge.setAttribute("class", "badge text-bg-warning rounded-pill") } else { privacyBadge.innerHTML = "Record"; privacyBadge.setAttribute("class", "badge text-bg-success rounded-pill") } listItem.appendChild(privacyBadge); if (i.recordOverride) { var recordOverrideBadge = document.createElement("span"); recordOverrideBadge.innerHTML = "Record override"; recordOverrideBadge.setAttribute("class", "badge text-bg-primary rounded-pill border border-danger") listItem.appendChild(recordOverrideBadge); } if (i.comments) { var commentsBadge = document.createElement("span"); commentsBadge.innerHTML = i.comments commentsBadge.setAttribute("class", "badge text-bg-info rounded-pill") listItem.appendChild(commentsBadge); } document.getElementById(elementName).appendChild(listItem); document.getElementById(elementName).appendChild(listItem); } } function refreshLists() { getCampScheduleAndAddToLists(); // Clear anything that's finished while (eventsOnNow.length >0 && eventsOnNow.at(0).hasEnded()) { eventsOnNow.shift(); } // Get anything that's started and add it to the "Happening now" list while (eventsOnNext.length > 0 && eventsOnNext.at(0).hasStarted()) { eventsOnNow.push(eventsOnNext.shift()); } } function handleCountdown() { countdown--; if (countdown == 0) { countdown = MAX_COUNTDOWN; // now = Date.now(); now = Date.now(); refreshLists(); getCampScheduleAndAddToLists(); populateListHtml("happeningNow", eventsOnNow, false); populateListHtml("happeningNext", eventsOnNext, true); } let counterElement = document.getElementById("counter"); counterElement.innerHTML = "Next refresh in " + countdown; }