1/** 2 * Events Calendar - Homepage section ----------------------------------------------------------------- 3 * Functionality for events calendar on homepage in section - events-calendar 4 */ 5let timezone = "America/Chicago"; // Timezone From Settings 6if (typeof jTimezone !== "undefined") { 7 timezone = jTimezone; 8} 9let today = new Date(); 10let todayClone = new Date(today); 11let currentMonth = today.getMonth(); 12let currentYear = today.getFullYear(); 13let firstDayOfCalendar = setFirstDayOfCalendar(todayClone, currentMonth, currentYear); 14let lastDayOfCalendar = setLastDayOfCalendar(todayClone, currentMonth, currentYear); 15let firstDayOfMonth = new Date(todayClone.setFullYear(currentYear, currentMonth, 1)); 16let lastDayOfMonth = new Date(todayClone.setFullYear(currentYear, currentMonth + 1, 0)); 17let todayDay = today.getDate(); 18let todayMonth = today.getMonth(); 19let todayYear = today.getFullYear(); 20let events = []; 21// let eventCategories = []; 22let eventCategory = null; 23let monthAndYear = ""; 24 25let eventsList, eventsListContainer, eventsListDate, calendarBodyTable; 26 27eventsList = waitForElement("#events-calendar-list"); 28eventsListContainer = waitForElement("#eventsCalendarListContainerContent"); 29eventsListDate = waitForElement("#events-calendar-list-date"); 30calendarBodyTable = waitForElement("#calendar-body"); 31 32var eventsListEmpty = "No content."; 33var calendar = document.getElementById("events-calendar-table"); 34 35var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "
35November", "December"]; 36var dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; 37 38var dayNamesContent = "<tr>"; 39dayNames.forEach((name) => { 40 dayNamesContent += `<th class="text-center text-capitalize" data-days="${name}">${name}</th>`; 41}); 42dayNamesContent += "</tr>"; 43if ((jActiveHomepageSections && jActiveHomepageSections.includes("Events-calendar") && window.location.pathname == "/") || window.location.pathname.includes("customizer")) { 44 let headMonth = document.getElementById("thead-month"); 45 if (headMonth !== null) { 46 headMonth.innerHTML = dayNamesContent; 47 monthAndYear = document.getElementById("monthAndYearTitle"); 48 } 49} 50 51// Handle next month 52async function eventCalendarNext() { 53 currentYear = currentMonth === 11 ? currentYear + 1 : currentYear; 54 currentMonth = (currentMonth + 1) % 12; 55 let cloneDate = new Date(today); 56 firstDayOfCalendar = setFirstDayOfCalendar(cloneDate, currentMonth, currentYear); 57 lastDayOfCalendar = setLastDayOfCalendar(cloneDate, currentMonth, currentYear); 58 firstDayOfMonth = new Date(cloneDate.setFullYear(currentYear, currentMonth, 1)); 59 lastDayOfMonth = new Date(cloneDate.setFullYear(currentYear, currentMonth + 1, 0)); 60 events = await getEvents(eventCategory); 61 showCalendar(currentMonth, currentYear); 62} 63 64// Handle previous month 65async function eventCalendarPrev() { 66 currentYear = currentMonth === 0 ? currentYear - 1 : currentYear; 67 currentMonth = currentMonth === 0 ? 11 : currentMonth - 1; 68 let cloneDate = new Date(today); 69 firstDayOfCalendar = setFirstDayOfCalendar(cloneDate, currentMonth, currentYear); 70 lastDayOfCalendar = setLastDayOfCalendar(cloneDate, currentMonth, currentYear); 71 firstDayOfMonth = new Date(cloneDate.setFullYear(currentYear, currentMonth, 1)); 72 lastDayOfMonth = new Date(cloneDate.setFullYear(currentYear, currentMonth + 1, 0)); 73 events = await getEvents(eventCategory); 74 showCalendar(currentMonth, currentYear); 75} 76 77// Show Calendar 78async function showCalendar(month, year) { 79 // console.log(events) 80 var firstDay = new Date(year, month).getDay(); 81 let tbl = document.getElementById("calendar-body"); 82 // console.log(tbl) 83 tbl.innerHTML = ""; 84 monthAndYear.innerHTML = months[month] + " " + year; 85 let calendarRows = getCalendarRows(); 86 // console.log(calendarRows) 87 // Create all calendar items 88 89 calendarRows.forEach((calendarRow) => { 90 var row = document.createElement("tr"); 91 calendarRow.forEach((day) => { 92 let cell = document.createElement("td"); 93 cell.setAttribute("role", "gridcell"); 94 cell.setAttribute("tabindex", "0"); 95 cell.setAttribute("aria-label", `${months[day.getMonth()]} ${day.getDate()}, ${day.getFullYear()}`); 96 97 if (isDateOfPreviousMonth(day)) { 98 cell.classList.add("previous-month"); 99 cell.setAttribute("aria-disabled", "true"); 100 cell.setAttribute("tabindex", "-1"); 101 } 102 103 if (isDateOfNextMonth(day)) { 104 cell.classList.add("next-month"); 105 cell.setAttribute("aria-disabled", "true"); 106 cell.setAttribute("tabindex", "-1"); 107 } 108 109 // check if calendar date has events 110 let dayEvents = getEventOnDay(day); 111 112 let div = document.createElement("div"); 113 div.className = "text-center"; 114 115 let todayCloneForCalendarCell = new Date(today); 116 todayCloneForCalendarCell.setHours(0, 0, 0, 0); 117 if (todayCloneForCalendarCell.getTime() === day.getTime()) { 118 cell.classList.add("tw-calendar-day-active"); 119 cell.setAttribute("aria-current", "date"); 120 } 121 122 if (dayEvents.length) { 123 cell.classList.add("event-item-btn"); 124 cell.setAttribute("aria-selected", "false"); 125 div.classList.add("cursor-pointer"); 126 div.innerHTML = `<div class="text-center">${day.getDate()}</div>`; 127 128 let moreEvents = 0; 129 130 dayEvents.forEach((event, index) => { 131 if (index > 1) { 132 moreEvents += 1; 133 } else { 134 let eventStartDate = new Date( 135 new Date(event.starts_at).toLocaleString("en", { 136 timeZone: timezone, 137 }), 138 ); 139 140 let timeString = eventStartDate
141 .toLocaleString("en-US", { 142 hour: "numeric", 143 minute: "numeric", 144 hour12: true, 145 }) 146 .replace(" AM", "am") 147 .replace(" PM", "pm"); 148 div.innerHTML += `<div class="tw-calendar-day-event-title ps-1">${timeString} ${event.name}</div>`; 149 } 150 }); 151 152 if (moreEvents) { 153 div.innerHTML += `<div class="text-start ps-1">+${moreEvents} more</div>`; 154 } 155 cell.setAttribute("aria-label", `${months[day.getMonth()]} ${day.getDate()}, ${day.getFullYear()}, ${dayEvents.length} event${dayEvents.length === 1 ? "" : "s"}`); 156 } else { 157 div.innerText = day.getDate(); 158 cell.setAttribute("aria-label", `${months[day.getMonth()]} ${day.getDate()}, ${day.getFullYear()}, no events`); 159 } 160 161 cell.appendChild(div); 162 163 cell.setAttribute("data-date", day); 164 cell.setAttribute("data-month", day.getMonth() + 1); 165 cell.setAttribute("data-year", day.getFullYear()); 166 cell.setAttribute("data-month_name", months[day.getMonth()]); 167 168 cell.addEventListener("keydown", (event) => { 169 if (event.key === "Enter" || event.key === " ") { 170 event.preventDefault(); 171 cell.click(); 172 } 173 }); 174 175 row.appendChild(cell); 176 }); 177 if (tbl) { 178 tbl.appendChild(row); 179 } 180 }); 181} 182 183function getCalendarRows() { 184 let calendarDays = getDaysBetweenDates(firstDayOfCalendar, lastDayOfCalendar); 185 186 let rows = []; 187 188 while (calendarDays.length > 0) { 189 rows.push(calendarDays.splice(0, 7)); 190 } 191 192 return rows; 193} 194 195function setFirstDayOfCalendar(date, month, year) { 196 let cloneDate = new Date(date); 197 let firstDayOfMonth = new Date(cloneDate.setFullYear(year, month, 1)); 198 return getFirstDayOfWeek(firstDayOfMonth, 0); 199} 200 201function setLastDayOfCalendar(date, month, year) { 202 let cloneDate = new Date(date); 203 let lastDayOfMonth = new Date(cloneDate.setFullYear(year, month + 1, 0)); 204 return getLastDayOfWeek(lastDayOfMonth, 6); 205} 206 207function getFirstDayOfWeek(dateObject, firstDayOfWeekIndex) { 208 const dayOfWeek = dateObject.getDay(), 209 firstDayOfWeek = new Date(dateObject), 210 diff = dayOfWeek >= firstDayOfWeekIndex ? dayOfWeek - firstDayOfWeekIndex : 6 - dayOfWeek; 211 212 firstDayOfWeek.setDate(dateObject.getDate() - diff); 213 firstDayOfWeek.setHours(0, 0, 0, 0); 214 215 return new Date(firstDayOfWeek); 216} 217 218function getLastDayOfWeek(dateObject, lastDayOfWeekIndex) { 219 const dayOfWeek = dateObject.getDay(), 220 lastDayOfWeek = new Date(dateObject), 221 diff = dayOfWeek >= lastDayOfWeekIndex ? dayOfWeek - lastDayOfWeekIndex : 6 - dayOfWeek; 222 223 lastDayOfWeek.setDate(dateObject.getDate() + diff); 224 lastDayOfWeek.setHours(0, 0, 0, 0); 225 226 return new Date(lastDayOfWeek); 227} 228 229function getDaysBetweenDates(startDate, endDate) { 230 let dates = []; 231 232 while (startDate.getTime() <= endDate.getTime()) { 233 let toPush = new Date(startDate); 234 dates.push(toPush); 235 let cloneStartDate = new Date(startDate); 236 startDate = new Date(cloneStartDate.setDate(startDate.getDate() + 1)); 237 } 238 239 return dates; 240} 241 242// check if date is of calendar current motnh 243function isDateOfCurrentMonth(date) { 244 return date.getTime() >= firstDayOfMonth.getTime() && date.getTime <= lastDayOfMonth.getTime(); 245} 246 247// check if date is from calendar previous month 248function isDateOfPreviousMonth(date) { 249 let firstDayOfMonthClone = new Date(firstDayOfMonth); 250 firstDayOfMonthClone.setHours(0, 0, 0, 0); 251 return date.getTime() < firstDayOfMonthClone.getTime(); 252} 253 254// check if date is from calendar next month 255function isDateOfNextMonth(date) { 256 let lastDayOfMonthClone = new Date(lastDayOfMonth); 257 lastDayOfMonthClone.setHours(0, 0, 0, 0); 258 return date.getTime() > lastDayOfMonthClone.getTime(); 259} 260 261// Get Events for selected period - Getting Events from HeyGov 262async function getEvents(categoryId = null, args = null) { 263 let startDate = formatDate(new Date(firstDayOfCalendar)) + "T00:00:00.000Z"; 264 let endDate = formatDate(new Date(lastDayOfCalendar)) + "T23:59:59.000Z"; 265 266 let categoryFilter = "";
267 if (categoryId && categoryId !== "0") { 268 categoryFilter += "&category=" + categoryId; 269 } 270 271 let requestUrl = `https://api.heygov.com/${jHeyGovJurisdiction}/events?`; 272 let response; 273 274 if (args) { 275 let urlSearchParamsArr = []; 276 277 if (args.date_start && args.date_end) { 278 urlSearchParamsArr.push(`date_start=${args.date_start}&date_end=${args.date_end}`); 279 startDate = args.date_start; 280 endDate = args.date_end; 281 } 282 283 if (args.limit) { 284 urlSearchParamsArr.push(`limit=${args.limit}`); 285 } 286 287 let urlSearchParams = urlSearchParamsArr.join("&"); 288 289 requestUrl += urlSearchParams; 290 291 response = await fetch(requestUrl + categoryFilter); 292 } else { 293 response = await fetch(requestUrl + "date_start=" + startDate + "&date_end=" + endDate + categoryFilter); 294 } 295 296 const events = await response.json(); 297 298 const isUnifiedDashboard = typeof jMainDashboard !== "undefined" && jMainDashboard === "unified-dashboard"; 299 // Meetings are not part of event categories, so skip merging when a category filter is active 300 if (!isUnifiedDashboard || (categoryId && categoryId !== "0")) { 301 return events; 302 } 303 304 // Fetch meetings and normalize them to the same shape as events 305 let meetings = []; 306 try { 307 const meetingsResponse = await fetch(`https://api.heygov.com/${jHeyGovJurisdiction}/meetings?api=1&status=publish&order=desc&limit=100&date_start=${startDate}&date_end=${endDate}`); 308 const meetingsData = await meetingsResponse.json(); 309 if (Array.isArray(meetingsData)) { 310 const rangeStart = new Date(startDate); 311 const rangeEnd = new Date(endDate); 312 meetings = meetingsData 313 .filter((m) => { 314 if (!m.starts_at) return false; 315 const d = new Date(m.starts_at); 316 return d >= rangeStart && d <= rangeEnd; 317 }) 318 .map((m) => ({ 319 pid: m.pid || m.id, 320 id: m.pid || m.id, 321 name: m.title, 322 description: m.description, 323 starts_at: m.starts_at, 324 ends_at: m.ends_at, 325 image_path: null, 326 image_url: null, 327 venues: [], 328 metadata: null, 329 _isMeeting: true, 330 })); 331 } 332 } catch (e) { 333 // meetings fetch failed â continue with events only 334 } 335 336 let combined = events.concat(meetings); 337 combined.sort((a, b) => new Date(a.starts_at) - new Date(b.starts_at)); 338 if (args && args.limit) { 339 combined = combined.slice(0, args.limit); 340 } 341 return combined; 342} 343 344// Get Event Categories for jurisdiction - Getting Them from HeyGov 345async function getEventCategories() { 346 const response = await fetch("https://api.heygov.com/" + jHeyGovJurisdiction + "/events/categories"); 347 // const eventCategories = await response.json() 348 // return eventCategories; 349} 350 351// Get Events on selected day of month 352function getEventOnDay(day) { 353 let dayEvents = []; 354 let cloneDay = new Date(day); 355 cloneDay.setHours(0, 0, 0, 0); 356 events.forEach((event) => { 357 let eventStartDate = new Date( 358 new Date(event.starts_at).toLocaleString("en", { 359 timeZone: timezone, 360 }), 361 ); 362 363 eventStartDate.setHours(0, 0, 0, 0); 364 if (eventStartDate.getTime() === cloneDay.getTime()) { 365 dayEvents.push(event); 366 } 367 }); 368 return dayEvents; 369} 370 371// Set date for events list (list of day events) 372function setEventsListDate(date) { 373 let value = ""; 374 value = formatProperDate(date); 375 eventsListDate.innerHTML = value; 376} 377 378// Generate list of day events 379async function generateDayEventsList(day, month, year) { 380 let dayClone = new Date(day); 381 382 let dayEvents = getEventOnDay(dayClone); 383 384 let eventsList = ""; 385 386 if (dayEvents.length > 0) { 387 let eDate = new Date( 388 new Date(dayEvents[0].starts_at).toLocaleString("en", { 389 timeZone: timezone, 390 }), 391 ); 392 setEventsListDate(eDate); 393 } else { 394 let eDate = new Date(new Date(year, month, dayClone.getDate()).toLocaleString("en")); 395 /* let eDate = new Date( 396 new Date(year, month, day).toLocaleString("en", { 397 timeZone: "Etc/UTC" 398 }) 399 ) */ 400 setEventsListDate(eDate); 401 } 402 403 let moreEvents = 0; 404
405 dayEvents.forEach((e, index) => { 406 if (index > 1) { 407 moreEvents += 1; 408 } else { 409 let startsAt = new Date( 410 new Date(e.starts_at).toLocaleString("en", { 411 timeZone: timezone, 412 }), 413 ); 414 let endsAt = new Date( 415 new Date(e.starts_at).toLocaleString("en", { 416 timeZone: timezone, 417 }), 418 ); 419 let startTime = formatAMPM(startsAt); 420 // let endTime = formatAMPM(endsAt) 421 if (e.metadata && e.metadata.hide_end_time) { 422 endTime = ""; 423 } 424 const isCanceled = e.status === "canceled"; 425 426 eventsList += `<a class="d-inline-flex mb-3 w-100" href="/${jEventsPage}/?${e._isMeeting ? "heygov-meeting" : "heygov-event"}=${e.pid}"> 427 <article class="tw-calendar-event-article bg-white w-100${isCanceled ? " event-canceled" : ""}"> 428 <div class="row"> 429 <div class="col-3 col-md-2"> 430 <div class="tw-calendar-event-article-time"><span>${startTime}</span></div> 431 </div> 432 <div class="col"> 433 <div class="tw-calendar-event-article-title mb-3"> 434 ${isCanceled ? '<span class="visually-hidden">Canceled: </span>' : ""}${e.name} 435 </div> 436 <div class="tw-calendar-event-article-venue">${e.venues && e.venues.length ? e.venues[0] : ""}</div> 437 </div> 438 </div> 439 </article> 440 </a>`; 441 } 442 }); 443 444 if (moreEvents) { 445 eventsList += `<div>+${moreEvents} more event${moreEvents === 1 ? "" : "s"}</div>`; 446 } 447 448 if (eventsList == "") { 449 eventsList = eventsListEmpty; 450 } 451 eventsListContainer.then((el) => (el.innerHTML = eventsList)); 452} 453 454// Format Time to AM/PM 455const formatAMPM = (date) => { 456 let hours = date.getHours(); 457 let minutes = date.getMinutes(); 458 const ampm = hours >= 12 ? "pm" : "am"; 459 460 hours %= 12; 461 hours = hours || 12; 462 minutes = minutes < 10 ? `0${minutes}` : minutes; 463 464 const strTime = `${hours}:${minutes}${ampm}`; 465 466 return strTime; 467}; 468 469// Format Date to Sunday, January 14, 2023 470const formatProperDate = (date) => { 471 let dateFormated = ""; 472 let weekday = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][date.getDay()]; 473 let monthName = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "
473November", "December"][date.getMonth()]; 474 let dateNumber = date.getDate(); 475 let yearNumber = date.getFullYear(); 476 dateFormated = weekday + ", " + monthName + " " + dateNumber + ", " + yearNumber; 477 return dateFormated; 478}; 479 480// Format date 481function formatDate(date = new Date()) { 482 const year = date.toLocaleString("default", { year: "numeric" }); 483 const month = date.toLocaleString("default", { 484 month: "2-digit", 485 }); 486 const day = date.toLocaleString("default", { day: "2-digit" }); 487 488 return [year, month, day].join("-"); 489} 490 491// Calculate days in month 492function daysInMonth(iMonth, iYear) { 493 return 32 - new Date(iYear, iMonth, 32).getDate(); 494} 495 496/** 497 * END Events Calendar - Homepage section --------------------------------------------------------------------------- 498 */ 499 500/** 501 * Events Homepage Section (events-carousel) -------------------------------------------------------------------------- 502 * Populates #tw-events-section-events-wrapper directly from HeyGov (same source as the events calendar above), 503 * then initializes the owl carousel. Items used to be rendered server-side from events.json at static-generation 504 * time, which meant the section went stale until the next republish. 505 */ 506var monthsShort = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 507 508function escapeHtmlAttr(value) { 509 return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); 510} 511 512// Fetch events from HeyGov for a date range; on unified-dashboard, meetings are 513// merged in and normalized to the same shape (mirrors the calendar's getEvents) 514async function fetchHeyGovEventsRange(startDate, endDate) { 515 const apiBase = getHeyGovApiBase();
516 const response = await fetch(apiBase + "/" + jHeyGovJurisdiction + "/events?date_start=" + startDate + "&date_end=" + endDate); 517 const eventsData = await response.json(); 518 const eventItems = Array.isArray(eventsData) ? eventsData : []; 519 520 const isUnifiedDashboard = typeof jMainDashboard !== "undefined" && jMainDashboard === "unified-dashboard"; 521 if (!isUnifiedDashboard) { 522 return eventItems; 523 } 524 525 let meetings = []; 526 try { 527 const meetingsResponse = await fetch(apiBase + "/" + jHeyGovJurisdiction + "/meetings?api=1&status=publish&order=desc&limit=100&date_start=" + startDate + "&date_end=" + endDate); 528 const meetingsData = await meetingsResponse.json(); 529 if (Array.isArray(meetingsData)) { 530 meetings = meetingsData 531 .filter((m) => m.starts_at) 532 .map((m) => ({ 533 pid: m.pid || m.id, 534 id: m.pid || m.id, 535 name: m.title, 536 starts_at: m.starts_at, 537 ends_at: m.ends_at, 538 metadata: null, 539 _isMeeting: true, 540 })); 541 } 542 } catch (e) { 543 // meetings fetch failed â continue with events only 544 } 545 546 return eventItems.concat(meetings); 547} 548 549// Build one carousel item, mirroring the markup the section template used to render server-side 550function buildHomepageEventArticle(e) { 551 const startsAt = new Date(new Date(e.starts_at).toLocaleString("en", { timeZone: timezone })); 552 const endRaw = e.ends_at || e.starts_at; 553 const endsAt = new Date(new Date(endRaw).toLocaleString("en", { timeZone: timezone })); 554
555 const month = monthsShort[startsAt.getMonth()]; 556 const day = String(startsAt.getDate()).padStart(2, "0"); 557 558 const hideEndTime = (e.metadata && e.metadata.hide_end_time) || !e.ends_at; 559 let timePeriod = hideEndTime ? formatAMPM(startsAt) : formatAMPM(startsAt) + " - " + formatAMPM(endsAt); 560 if (timePeriod === "12:00 AM - 11:59 PM") { 561 timePeriod = "All day"; 562 } 563 564 const eventId = e.pid != null ? e.pid : e.id; 565 const link = "/" + jEventsPage + "/?" + (e._isMeeting ? "heygov-meeting" : "heygov-event") + "=" + eventId; 566 const name = escapeHtmlAttr(e.name || ""); 567 const itemLabel = escapeHtmlAttr((e.name || "") + " on " + month + " " + day + " at " + timePeriod); 568 const linkLabel = escapeHtmlAttr("View details for " + (e.name || "") + " on " + month + " " + day); 569 const startsAtAttr = escapeHtmlAttr(e.starts_at); 570 const startsAtLocalAttr = escapeHtmlAttr(e.starts_at_local || ""); 571 const isCanceled = e.status === "canceled"; 572 573 return ( 574 '<article class="p-3 d-flex justify-content-center align-items-center event-item' + (isCanceled ? " event-canceled" : "") + '" role="option" aria-label="' + itemLabel + (isCanceled ? " (canceled)" : "") + '" data-local-date="' + startsAtLocalAttr + '" data-date="' + startsAtAttr + '">' + 575 '<div class="p-2 position-relative">' + 576 (isCanceled ? '<span class="tw-event-canceled-badge">Canceled</span>' : "") + 577 '<div class="tw-date tw-secondary d-flex justify-content-center align-items-center text-uppercase position-relative mb-4">' + 578 '<a class="text-decoration-none" href="' + link + '" aria-label="' + linkLabel + '">' + 579 '<time datetime="' + startsAtAttr + '">' + month + " " + day + "</time>" + 580 "</a>" + 581 "</div>" + 582 '<div class="tw-event-name fw-bold tw-secondary text-center">' + name + "</div>" + 583 '<div class="tw-event-venue tw-primary-dark text-center mt-2"></div>' + 584 '<div class="tw-event-time text-center mt-2 tw-primary"><time datetime="' + startsAtAttr + '">' + timePeriod + "</time></div>" + 585 "</div>" + 586 "</article>" 587 ); 588} 589 590// Initialize the events owl carousel â called AFTER items are injected, 591// otherwise owl initializes over an empty wrapper and never shows them 592function initHomepageEventsCarousel() { 593 let owlEvents = $("#tw-events .owl-carousel"); 594 let owlOptionsEvents = { 595 loop: false, 596 dots: false, 597 nav: true, 598 navText: ['<i class="fas fa-angle-left fa-2x" aria-hidden="true"></i>', '<i class="fas fa-angle-right fa-2x" aria-hidden="true"></i>'], 599 navClass: ["owl-prev tw-secondary", "owl-next float-end tw-secondary"], 600 items: 5, 601 autoWidth: false, 602 center: false, 603 URLhashListener: true, 604 autoplayHoverPause: false, 605 startPosition: "URLHash", 606 responsive: { 607 0: { 608 items: 2, 609 }, 610 768: { 611 items: 3, 612 }, 613 992: { 614 items: 4, 615 }, 616 1184: { 617 items: 5, 618 }, 619 }, 620 }; 621 622 function eventsOwlA11yFix() { 623 $("#tw-events .owl-prev").attr("aria-label", "Previous events").removeAttr("role"); 624 $("#tw-events .owl-next").attr("aria-label", "Next events").removeAttr("role"); 625 } 626 627 owlEvents.on("initialized.owl.carousel refreshed.owl.carousel resized.owl.carousel", function () { 628 eventsOwlA11yFix(); 629 }); 630 631 var owlActiveEvents = owlEvents.owlCarousel(owlOptionsEvents); 632 633 eventsOwlA11yFix(); 634 setTimeout(eventsOwlA11yFix, 100); 635 636 owlActiveEvents.on("changed.owl.carousel", function () { 637 eventsOwlA11yFix(); 638 }); 639} 640 641// Populate the homepage events section and start its carousel 642async function populateHomepageEventsSection() { 643 const $wrapper = $("#tw-events-section-events-wrapper"); 644 if ($wrapper.length === 0) { 645 return; 646 } 647 const $loading = $("#tw-events-loading"); 648 649 // Everything below is guarded so the loading placeholder can never get stuck: 650 // any unexpected error falls through to the catch, which clears it 651 try { 652 let items = []; 653 if (typeof jHeyGovJurisdiction !== "undefined" && jHeyGovJurisdiction !== "") { 654 const rangeStart = new Date(); 655 const rangeEnd = new Date(); 656 rangeEnd.setMonth(rangeEnd.getMonth() + 9);
657 try { 658 items = await fetchHeyGovEventsRange(formatDate(rangeStart), formatDate(rangeEnd)); 659 } catch (err) { 660 console.error("Error loading HeyGov events for homepage section:", err); 661 items = []; 662 } 663 } 664 665 // Filter out past items client-side: the HeyGov /meetings endpoint ignores 666 // date_start/date_end (returns the latest <=100, desc), so the range params 667 // alone don't guarantee upcoming-only results 668 const townNow = new Date(new Date().toLocaleString("en", { timeZone: timezone })); 669 const startOfToday = new Date(townNow.getFullYear(), townNow.getMonth(), townNow.getDate()); 670 671 const count = parseInt($wrapper.attr("data-count"), 10) || 5; 672 items = items 673 .filter((e) => e && e.starts_at) 674 .filter((e) => new Date(new Date(e.starts_at).toLocaleString("en", { timeZone: timezone })) >= startOfToday) 675 .sort((a, b) => new Date(a.starts_at) - new Date(b.starts_at)) 676 .slice(0, count); 677 678 if (items.length === 0) { 679 $loading.remove(); 680 $wrapper.removeClass("owl-carousel"); 681 $wrapper.html('<div class="mb-4 text-center">No data.</div>'); 682 return; 683 } 684 685 // Inject items, drop the loading placeholder and init owl in the same synchronous 686 // block â the wrapper is display:none until owl adds .owl-loaded, so removing the 687 // placeholder any earlier or later would make the section height flicker 688 $wrapper.html(items.map(buildHomepageEventArticle).join("")); 689 $loading.remove(); 690 initHomepageEventsCarousel(); 691 } catch (err) { 692 console.error("Error rendering homepage events section:", err); 693 $loading.remove(); 694 $wrapper.removeClass("owl-carousel"); 695 $wrapper.html('<div class="mb-4 text-center">No data.</div>'); 696 } 697} 698 699// Run in a dedicated ready handler: the main document.ready block below is large and 700// an unrelated error thrown there would stop it before reaching this section (jQuery 3 701// isolates ready handlers, so this one runs regardless) 702$(function () { 703 populateHomepageEventsSection(); 704}); 705 706/** 707 * END Events Homepage Section ---------------------------------------------------------------------------------------- 708 */ 709 710/** 711 * Meetings --------------------------------------------------------------------------------------------------------- 712 * Function is created with vanila JavaScript 713 */ 714var meetingItemsPerTable = 10; 715var searchCategory = ""; 716var dateFrom = ""; 717var dateTo = ""; 718var categories = []; 719var pager = 1; 720var pagerCategory = 0; 721var meetingsByCategory = {}; 722 723function getHeyGovApiBase() { 724 if (typeof jHeyGovApi === "string" && jHeyGovApi !== "") return jHeyGovApi.replace(/\/+$/, ""); 725 return "https://api.heygov.com"; 726} 727 728function formatHeyGovMeetingDate(startsAt) { 729 if (!startsAt) return ""; 730 const options = { year: "numeric", month: "long", day: "numeric" }; 731 if (typeof jTimezone === "string" && jTimezone !== "") options.timeZone = jTimezone; 732 return new Date(startsAt).toLocaleDateString("en-US", options); 733} 734 735function normalizeHeyGovMeeting(meeting) { 736 return { 737 id: meeting.id, title: meeting.title || "", description: meeting.description || "", notes: meeting.notes || "", 738 categories: Array.isArray(meeting.categories) ? meeting.categories : [], 739 departments: Array.isArray(meeting.departments) ? meeting.departments : [], 740 agenda_file_path: meeting.agenda_file_url || "", agenda_text: meeting.agenda_text || "", 741 agenda_pack_file_path: meeting.agenda_pack_file_url || "", 742 meeting_minutes_file_path: meeting.meeting_minutes_file_url || "", 743 audio_public_url: meeting.audio_public_url || "", video_public_url: meeting.video_public_url || "", 744 meeting_date: formatHeyGovMeetingDate(meeting.starts_at), 745 additional_file_path: meeting.additional_file_url || "", 746 additional_url: meeting.metadata && meeting.metadata.additional_url ? meeting.metadata.additional_url : "", 747 additional_doc: meeting.metadata && meeting.metadata.additional_doc ? meeting.metadata.additional_doc : "Document", 748 status: meeting.status || "publish", 749 }; 750} 751 752async function fetchAllHeyGovMeetings(page = 1, collected = []) { 753 const limit = 200; 754 const url = `${getHeyGovApiBase()}/${jHeyGovJurisdiction}/meetings?api=1&order=desc&limit=${limit}&page=${page}`; 755 const response = await fetch(url); 756 if (!response.ok) throw new Error(`HeyGov meetings request failed with status ${response.status}`); 757 const pageMeetings = await response.json(); 758 if (!Array.isArray(pageMeetings)) throw new Error("HeyGov meetings response is not an array"); 759 const meetings = collected.concat(pageMeetings); 760 return pageMeetings.length === limit ? fetchAllHeyGovMeetings(page + 1, meetings) : meetings; 761} 762 763function groupMeetingsByTaxonomy(meetings, taxonomy, meetingField) { 764 meetingsByCategory = {};
765 meetings.forEach((meeting) => { 766 meeting[meetingField].forEach((taxonomyId) => { 767 const key = String(taxonomyId); 768 if (!meetingsByCategory[key]) meetingsByCategory[key] = []; 769 meetingsByCategory[key].push(meeting); 770 }); 771 }); 772 return taxonomy.filter((item) => meetingsByCategory[String(item.id)]); 773} 774 775async function fetchHeyGovMeetingsData() { 776 const isUnifiedDashboard = typeof jMainDashboard !== "undefined" && jMainDashboard === "unified-dashboard"; 777 const taxonomyPath = isUnifiedDashboard ? "departments" : "meetings/categories"; 778 const meetingField = isUnifiedDashboard ? "departments" : "categories"; 779 const apiBase = `${getHeyGovApiBase()}/${jHeyGovJurisdiction}`; 780 const [taxonomyResponse, meetings] = await Promise.all([fetch(`${apiBase}/${taxonomyPath}`), fetchAllHeyGovMeetings()]); 781 782 if (!taxonomyResponse.ok) { 783 throw new Error(`HeyGov ${taxonomyPath} request failed with status ${taxonomyResponse.status}`); 784 } 785 const taxonomy = await taxonomyResponse.json(); 786 if (!Array.isArray(taxonomy)) { 787 throw new Error(`HeyGov ${taxonomyPath} response is not an array`); 788 } 789 790 const publicMeetings = meetings 791 .filter((meeting) => meeting && (meeting.status === "publish" || meeting.status === "canceled")) 792 .map(normalizeHeyGovMeeting); 793 const grouped = groupMeetingsByTaxonomy(publicMeetings, taxonomy, meetingField); 794 return isUnifiedDashboard ? grouped : sortMeetingCategoriesBySettings(grouped); 795} 796 797// Legacy sites: order categories as set in mc2 client settings (jMeetingsCategories 798// from settings.js, sorted by `order`); categories missing from it go last. 799function sortMeetingCategoriesBySettings(taxonomy) { 800 if (typeof jMeetingsCategories === "undefined") return taxonomy; 801 try { 802 const orderedIds = JSON.parse(jMeetingsCategories).map((category) => String(category.id)); 803 return taxonomy 804 .map((category, index) => ({ category, index })) 805 .sort((a, b) => { 806 const orderA = orderedIds.indexOf(String(a.category.id)); 807 const orderB = orderedIds.indexOf(String(b.category.id)); 808 return (orderA === -1 ? orderedIds.length + a.index : orderA) - (orderB === -1 ? orderedIds.length + b.index : orderB); 809 }) 810 .map((item) => item.category); 811 } catch (error) { 812 console.error("Error parsing jMeetingsCategories:", error); 813 return taxonomy; 814 } 815} 816 817function showMeetingsLoadError(error) { 818 console.error("Error loading categories", error); 819 const content = document.getElementById("juniperMeetings"); 820 if (content) content.innerHTML = '<p class="meetings-page-loading-error" role="alert">Unable to load agendas and minutes. Please try again later.</p>'; 821} 822 823// Unified always uses departments; legacy always uses meeting categories. 824function fetchCategories() { 825 fetchHeyGovMeetingsData() 826 .then((data) => { 827 categories = data; 828 console.log("Categories Loaded â "); 829 if (window.location.pathname.includes(jmeetingPage)) { 830 generateMeetingTables(); 831 generateCategoriesSelect(); 832 } 833 }) 834 .catch((error) => { 835 showMeetingsLoadError(error); 836 }); 837} 838 839// Search Meetings by Date or Category 840function searchMeetings() { 841 let categoryId = document.getElementById("categoriesSelect").value; 842 searchCategory = categoryId; 843 dateFrom = document.getElementById("dateFrom").value; 844 dateTo = document.getElementById("dateTo").value; 845 generateMeetingTables(); 846} 847 848// Generate Categories Select Input for search 849function generateCategoriesSelect() { 850 var categoriesSelect = document.getElementById("categoriesSelect"); 851 var categoriesSelectOptions = `<option value="" aria-label="All Categories">All Categories</option>`; 852 categories.forEach((category) => { 853 categoriesSelectOptions += `<option value="${category.id}" aria-label="${category.name}">${category.name}</option>`; 854 }); 855 categoriesSelect.innerHTML = categoriesSelectOptions; 856} 857 858// Load more items in table per category 859function loadMore(categoryId) { 860 if (categoryId == pagerCategory) { 861 pager = pager + 1; 862 } else { 863 pagerCategory = categoryId; 864 pager = 2; 865 } 866 var loader = document.getElementById("load-" + categoryId); 867 loader.classList.add("fa-spin"); 868 let category = categories.find((element) =>
868 element.id == categoryId); 869 category.perPage = meetingItemsPerTable * pager; 870 populateCategoryTable(category); 871} 872 873// Generate tables for categories with no data 874function generateCategoryEmptyTable(category) { 875 // Get a reference to the table element 876 var content = document.getElementById("juniperMeetings"); 877 let html = `<div id="Category-${category.id}"> 878 <h3 class="twd-category-name"><span>${category.name}</span></h3> 879 <table class="table table-striped twd-table tw-meeting-repo-table"> 880 <thead> 881 <tr>`; 882 if (jMeetingActiveColumns.includes("meeting_date")) { 883 html += `<th class="meeting-date-column">Meeting Date</th>`; 884 } 885 if (jMeetingActiveColumns.includes("title")) { 886 html += `<th class="agenda-column">Name</th>`; 887 } 888 if (jMeetingActiveColumns.includes("agenda")) { 889 html += `<th class="agenda-column">Agenda</th>`; 890 } 891 if (jMeetingActiveColumns.includes("agenda_packet")) { 892 html += `<th class="agenda-packet-column">Agenda Packet</th>`; 893 } 894 if (jMeetingActiveColumns.includes("minutes")) { 895 html += `<th class="meeting-minutes-column">Meeting Minutes</th>`; 896 } 897 if (jMeetingActiveColumns.includes("audio")) { 898 html += `<th class="audio-column">Audio</th>`; 899 } 900 if (jMeetingActiveColumns.includes("video")) { 901 html += `<th class="video-column">Video</th>`; 902 } 903 if (jMeetingActiveColumns.includes("additional_documents")) { 904 html += `<th class="additional-documents-column">Additional Documents</th>`; 905 } 906 if (jMeetingActiveColumns.includes("notes")) { 907 html += `<th class="notes-column">Notes</th>`; 908 } 909 html += `</tr> 910 </thead> 911 <tbody id="table-category-${category.id}"> 912 913 </tbody> 914 <tfoot id="table-category-footer-${category.id}"></tfoot> 915 </table> 916 </div>`; 917 918 content.innerHTML = content.innerHTML + html; 919} 920 921// Check is meeting date in range between search dates 922function inRange(meetingDate) { 923 let show = false; 924 if (dateFrom !== "") { 925 dateF = Date.parse(dateFrom); 926 } else { 927 dateF = ""; 928 } 929 if (dateTo !== "") { 930 dateT = Date.parse(dateTo); 931 } else { 932 dateT = ""; 933 } 934 if (dateF !== "" && dateT !== "") { 935 if (dateF <= meetingDate && dateT >= meetingDate) { 936 show = true; 937 } 938 } 939 if (dateF !== "" && dateT == "") { 940 if (dateF <= meetingDate) { 941 show = true; 942 } 943 } 944 if (dateF == "" && dateT !== "") { 945 if (dateT >= meetingDate) { 946 show = true; 947 } 948 } 949 if (dateF == "" && dateT == "") { 950 show = true; 951 } 952 return show; 953} 954 955// Filtering meetings by date 956function filterDateData(data) { 957 let filteredData = []; 958 data.forEach((item) => {
959 let checkDate = Date.parse(item.meeting_date); 960 if (inRange(checkDate)) { 961 filteredData.push(item); 962 } 963 }); 964 return filteredData; 965} 966 967// Generate data for category table 968function populateCategoryTable(category) { 969 Promise.resolve(meetingsByCategory[String(category.id)] || []) 970 .then((data) => { 971 let html = ""; 972 data = filterDateData(data); 973 // Generate table rows for the municipalities 974 for (var i = 0; i < category.perPage; i++) { 975 if (i >= data.length) { 976 break; 977 } 978 const isCanceled = data[i].status === "canceled"; 979 html += `<tr${isCanceled ? ' class="meeting-canceled"' : ""}>`; 980 981 if (jMeetingActiveColumns.includes("meeting_date")) { 982 html += `<td class="meeting-date-column">${new Date(data[i].meeting_date).toLocaleDateString("default", { 983 dateStyle: "medium", 984 })}</td>`; 985 } 986 987 if (jMeetingActiveColumns.includes("title")) { 988 html += `<td class="meeting-date-column"> 989 ${data[i].title} 990 </td>`; 991 } 992 993 if (jMeetingActiveColumns.includes("agenda")) { 994 if (data[i].agenda_file_path) { 995 html += `<td class="agenda-column"> 996 <a href="${data[i].agenda_file_path}" target="_blank" aria-label="Download PDF Agenda for ${data[i].title} ${data[i].meeting_date}"><i class="fa-solid fa-file-pdf"></i> Agenda</a> 997 </td>`; 998 } else { 999 html += `<td class="agenda-column"></td>`; 1000 } 1001 } 1002 1003 if (jMeetingActiveColumns.includes("agenda_packet")) { 1004 if (data[i].agenda_pack_file_path) { 1005 html += `<td class="agenda-packet-column"> 1006 <a href="${data[i].agenda_pack_file_path}" target="_blank" aria-label="Download PDF Agenda Pack for ${data[i].title} ${data[i].meeting_date}"><i class="fa-solid fa-file-pdf"></i> Packet</a> 1007 </td>`; 1008 } else { 1009 html += `<td class="agenda-packet-column"></td>`; 1010 } 1011 } 1012 1013 if (jMeetingActiveColumns.includes("minutes")) { 1014 if (data[i].meeting_minutes_file_path) { 1015 html += `<td class="meeting-minutes-column"> 1016 <a href="${data[i].meeting_minutes_file_path}" target="_blank" aria-label="Download PDF Minutes for ${data[i].title} ${data[i].meeting_date}"><i class="fa-solid fa-file-pdf"></i> Minutes</a> 1017 </td>`; 1018 } else { 1019 html += `<td class="meeting-minutes-column"></td>`; 1020 } 1021 } 1022 1023 if (jMeetingActiveColumns.includes("audio")) { 1024 if (data[i].audio_public_url && data[i].audio_public_url !== "") { 1025 html += `<td class="audio-column"> 1026 <a href="${data[i].audio_public_url}" target="_blank" aria-label="Download Audio for ${data[i].title} ${data[i].meeting_date}"><i class="fa-solid fa-file-pdf"></i> Audio</a> 1027 </td>`; 1028 } else { 1029 html += `<td class="audio-column"></td>`; 1030 } 1031 } 1032 1033 if (jMeetingActiveColumns.includes("video")) { 1034 if (data[i].video_public_url && data[i].video_public_url !== "") { 1035 html += `<td class="video-column"> 1036 <a href="${data[i].video_public_url}" target="_blank" aria-label="Download Video for ${data[i].title} ${data[i].meeting_date}"><i class="fa-solid fa-file-video"></i> Video</a> 1037 </td>`; 1038 } else { 1039 html += `<td class="video-column"></td>`; 1040 } 1041 } 1042 1043 if (jMeetingActiveColumns.includes("additional_documents")) { 1044 if (data[i].additional_file_path && data[i].additional_file_path !== "") { 1045 html += `<td class="additional-documents-column"> 1046 <a href="${data[i].additional_file_path}" target="_blank" aria-label="Download Video for ${data[i].title} ${data[i].meeting_date}"><i class="fa-solid fa-file-pdf"></i> ${data[i].additional_doc}</a> 1047 </td>`; 1048 } else if (data[i].additional_url && data[i].additional_url !== "") { 1049 html += `<td class="additional-documents-column"> 1050 <a href="${data[i].additional_url}" target="_blank" aria-label="Download Video for ${data[i].title} ${data[i].meeting_date}"><i class="fa-solid fa-file-pdf"></i> ${data[i].additional_doc}</a> 1051 </td>`; 1052 } else { 1053 html += `<td class="additional-documents-column"></td>`;
1054 } 1055 } 1056 1057 if (jMeetingActiveColumns.includes("notes")) { 1058 if (data[i].notes && data[i].notes !== "") { 1059 html += `<td class="notes-column"> 1060 ${data[i].notes} 1061 </td>`; 1062 } else { 1063 html += `<td class="notes-column"></td>`; 1064 } 1065 } 1066 1067 html += `</tr>`; 1068 } 1069 1070 if (data.length == 0) { 1071 html += `<tr><td class="text-center" colspan="8">No data in this category</td></tr>`; 1072 } 1073 1074 if (category.perPage < data.length) { 1075 let tfootElement = document.getElementById(`table-category-footer-${category.id}`); 1076 tfoot = `<tr> 1077 <td colspan="8"> 1078 <a 1079 tabindex="0" 1080 aria-label="View more agendas & minutes" 1081 role="button" 1082 onclick="loadMore(${category.id})" 1083 class="load-more" 1084 > 1085 <i id="load-${category.id}" class="fa fa-refresh"></i> View more 1086 </a> 1087 </td> 1088 </tr>`; 1089 tfootElement.innerHTML = tfoot; 1090 } else { 1091 let tfootElement = document.getElementById(`table-category-footer-${category.id}`); 1092 tfootElement.innerHTML = ""; 1093 } 1094 let categoryContent = document.getElementById("table-category-" + category.id); 1095 categoryContent.innerHTML = html; 1096 }) 1097 .catch((error) => { 1098 console.error("Error fetching data:", error); 1099 }); 1100} 1101 1102// Generate meering tables 1103function generateMeetingTables() { 1104 var content = document.getElementById("juniperMeetings"); 1105 content.innerHTML = ""; 1106 // Initiali populate tables 1107 categories.forEach((category) => { 1108 if (searchCategory == "") { 1109 category.perPage = meetingItemsPerTable; 1110 generateCategoryEmptyTable(category); 1111 populateCategoryTable(category); 1112 } else { 1113 if (searchCategory == category.id) { 1114 category.perPage = meetingItemsPerTable; 1115 generateCategoryEmptyTable(category); 1116 populateCategoryTable(category); 1117 } 1118 } 1119 }); 1120} 1121 1122// END Meetings ---------------------------------------------------------------------------------------------------------------- 1123 1124/** 1125 * Handle category selection from parameter 1126 */ 1127 1128// Function to get the value of a URL parameter by name 1129function getParameterByName(name) { 1130 const urlParams = new URLSearchParams(window.location.search); 1131 return urlParams.get(name); 1132} 1133 1134function updateURLParameter(param, paramVal) { 1135 const url = new URL(window.location.href); 1136 url.searchParams.set(param, paramVal); 1137 window.history.replaceState(null, null, url); 1138} 1139 1140function handleMeetingCategoryFromUrl() { 1141 // Get the 'category' parameter from the URL 1142 const categoryText = getParameterByName("category"); 1143 1144 // Find the select box 1145 const selectBox = document.getElementById("categoriesSelect"); 1146 1147 // If the category parameter exists, find the option with matching inner text 1148 if (categoryText) { 1149 setTimeout(() => { 1150 const options = selectBox.options; 1151 for (let i = 0; i < options.length; i++) { 1152 if (options[i].innerText === categoryText) { 1153 selectBox.selectedIndex = i; 1154 break; 1155 } 1156 } 1157 // Automatically click the button after selecting the option 1158 document.getElementById("searchMeetings").click(); 1159 }, 500); 1160 } 1161 1162 // Listen for changes in the select box 1163 const setCat = document.getElementById("searchMeetings"); 1164 if (setCat) { 1165 document.getElementById("searchMeetings").addEventListener("click", function () { 1166 const selectedText = selectBox.options[selectBox.selectedIndex].text; 1167 // Update the URL with the new category parameter 1168 updateURLParameter("category", selectedText); 1169 }); 1170 } 1171} 1172 1173$(document).ready(async function ($) { 1174 handleMeetingCategoryFromUrl(); 1175 // EVENTS CALENDAR HOMEPAGE --------------------------------------------------- 1176 // eventCategories = await getEventCategories(); 1177 // Load events from HeyGov 1178 eventCategory = $("#events-calendar-2019 .tw-data").data("event_categories_filter"); 1179 1180 if (eventCategory) { 1181 eventCategory = eventCategory.trim(); 1182 let lastChar = eventCategory.slice(-1); 1183 if (lastChar == ",") { 1184 eventCategory = eventCategory.slice(0, -1); 1185 } 1186 } 1187 // Handle events only if pathname is / and events-calendar is active on homepage 1188 if ((jActiveHomepageSections && jActiveHomepageSections.includes("Events-calendar") && window.location.pathname == "/") || window.location.pathname.includes("customizer")) { 1189 calendarBodyTable.then(async () => { 1190 events = await getEvents(); 1191 showCalendar(currentMonth, currentYear); 1192 setEventsListDate(today); 1193 generateDayEventsList(today, todayMonth, todayYear); 1194 }); 1195 } 1196 // Events calendar item click 1197 $("body").on("click", ".event-item-btn", function () { 1198 // tw-calendar-day-active 1199 $("#events-calendar-table .event-item-btn").removeClass("tw-calendar-day-active").attr("aria-selected", "false"); 1200 $(this).addClass("tw-calendar-day-active").attr("aria-selected", "true"); 1201 generateDayEventsList($(this).data("date"), $(this).data("month"), $(this).data("year")); 1202 }); 1203 // Events calendar next month 1204 $("body").on("click", ".event-calendar-next", function () { 1205 eventCalendarNext(); 1206 }); 1207 // Events calendar prev month 1208 $("body").on("click", ".event-calendar-prev", function () { 1209 eventCalendarPrev(); 1210 }); 1211 // Events calendar filter by category 1212 $("body").on("click", "#eventsCalendarListContainer .tw-event-category-tab", async function (e) { 1213 let targetIsActive = $(this).hasClass("active"); 1214 1215 $("#eventsCalendarListContainer .tw-event-category-tab").removeClass("active"); 1216 eventCategory = null; 1217 1218 if (!targetIsActive) { 1219 $(this).addClass("active"); 1220 eventCategory = $(this).data("tw-event-category-id"); 1221 } 1222 events = await getEvents(eventCategory); 1223 showCalendar(currentMonth, currentYear); 1224 generateDayEventsList(today, todayMonth, todayYear); 1225 }); 1226 1227 // #EVENTS CALENDAR HOMEPAGE --------------------------------------------------- 1228 1229 // Fetch categories for Meetings 1230 if (window.location.pathname == "/" + jmeetingPage || window.location.pathname == "/" + jmeetingPage + "/") { 1231 fetchCategories(); 1232 } 1233 1234 let $window = $(window), 1235 height = $window.height(), 1236 width = $window.width(), 1237 navheight = $("#defaultHeader #topHeader").height(); 1238 1239 let scrollDirection, oldScrollDirection; 1240 let oldScrollTop = 0; 1241 let fixedInitClass = "fixed-init"; 1242 1243 let headerHeight = $("#defaultHeader").height(); 1244 let headerBottomHeight = $("#defaultHeader #bottomHeader").height(); 1245 if ($("div#menuHero ul").length) { 1246 let heroLineMenuTopOffset = $("div#menuHero ul").offset().top; 1247 let headerHeroMenuDistance = heroLineMenuTopOffset - headerHeight; 1248 } 1249 let stickyPoint = false;
1250 $window.on("resize orientationchange", setMeasures); 1251 1252 // Menu snippet 1253 let viewWidth = document.documentElement.clientWidth; 1254 let elements = document.querySelectorAll(".menu-level-one li > ul"); 1255 const resizeWatcher = new ResizeObserver((entries) => { 1256 for (const entry of entries) { 1257 let elementObject = entry.target.getBoundingClientRect(); 1258 1259 if (elementObject.right > viewWidth) { 1260 entry.target.style.left = "auto"; 1261 entry.target.style.right = "100%"; 1262 } 1263 } 1264 }); 1265 1266 elements.forEach((element) => { 1267 resizeWatcher.observe(element); 1268 }); 1269 1270 function setMeasures() { 1271 ((height = $window.height()), (width = $window.width()), (navheight = $("#defaultHeader #topHeader").height())); 1272 1273 headerHeight = $("#defaultHeader").height(); 1274 headerBottomHeight = $("#defaultHeader #bottomHeader").height(); 1275 1276 if ($("div#menuHero ul").length) { 1277 heroLineMenuTopOffset = $("div#menuHero ul").offset().top; 1278 headerHeroMenuDistance = heroLineMenuTopOffset - headerHeight; 1279 } 1280 } 1281 1282 // Function Menu Hero Sticky 1283 // function sticky() { 1284 // var scrollTop = $window.scrollTop() 1285 // if (scrollTop > oldScrollTop) { 1286 // scrollDirection = "down" 1287 // } else { 1288 // scrollDirection = "up" 1289 // } 1290 // oldScrollTop = scrollTop 1291 1292 // if (!$("#defaultHeader #bottomHeader").hasClass(scrollDirection)) { 1293 // $("#defaultHeader #bottomHeader").removeClass(oldScrollDirection).addClass(scrollDirection) 1294 // } 1295 1296 // if (scrollTop > navheight) { 1297 // $("#defaultHeader").addClass("fixed-header") 1298 // $("#defaultHeader #bottomHeader").addClass("fixed") 1299 // $("#defaultHeader #bottomHeader").removeClass("fixed-init").addClass(fixedInitClass) 1300 // $("#defaultHeader nav.navbar").addClass("fixed-navbar") 1301 // fixedInitClass = "" 1302 // $('body [data-bs-toggle="tooltip"]').tooltip("enable") 1303 // if ($("div#menuHero ul").length) { 1304 // if (!stickyPoint) { 1305 // $("div#menuHero").css({ 1306 // top: "180px" 1307 // }) 1308 // } 1309 // stickyPoint = true 1310 // $("div#menuHero").addClass("fixed-side-menu").removeClass("absolute-side-menu") 1311 // } 1312 // } else { 1313 // $("#defaultHeader").removeClass("fixed-header") 1314 // $("#defaultHeader #bottomHeader").removeClass("fixed") 1315 // $("#defaultHeader nav.navbar").removeClass("fixed-navbar") 1316 // $('body [data-bs-toggle="tooltip"]').tooltip("disable") 1317 1318 // fixedInitClass = "fixed-init" 1319 1320 // if ($("div#menuHero ul").length) { 1321 // if (stickyPoint) { 1322 // $("div#menuHero").css({ 1323 // top: 0 1324 // }) 1325 // } 1326 // stickyPoint = false 1327 // $("div#menuHero").addClass("absolute-side-menu").removeClass("fixed-side-menu") 1328 // } 1329 // } 1330 // oldScrollDirection = scrollDirection 1331 // } 1332 1333 // $window.on("scroll resize orientationchange", sticky) 1334 1335 // Back to top button 1336 $(window).scroll(function () { 1337 if ($(this).scrollTop() != 0) { 1338 $("#back-to-top").fadeIn(); 1339 } else { 1340 $("#back-to-top").fadeOut(); 1341 } 1342 }); 1343 1344 $("#back-to-top").on("click touchstart", function (e) { 1345 e.preventDefault(); 1346 $("body,html").animate({ scrollTop: 0 }, 700); 1347 return false; 1348 }); 1349 // Tickerbar marquee ----------------------------------------------------------------------------------------------- 1350 $(".marquee").marquee({ 1351 //duration in milliseconds of the marquee 1352 // duration: 7000, 1353 speed: 120, 1354 //gap in pixels between the tickers 1355 gap: 50, 1356 //time in milliseconds before the marquee will start animating 1357 delayBeforeStart: 0, 1358 //'left' or 'right' 1359 direction: "left", 1360 //true or false - should the marquee be duplicated to show an effect of continues flow 1361 duplicated: false, 1362 //true or false - should the marquee animation pause on hover 1363 pauseOnHover: true, 1364 }); 1365 1366 let owl = $("#sectionNewsDefault .owl-carousel"); 1367 let owlOptions = { 1368 stagePadding: 0, 1369 loop: true, 1370 dots: true, 1371 margin: 0, 1372 nav: true, 1373 navText: ['<i class="fas fa-angle-left fa-2x" aria-hidden="true"></i>', '<i class="fas fa-angle-right fa-2x" aria-hidden="true"></i>'], 1374 // navContainer: '.custom-nav', 1375 navClass: ["owl-prev text-white", "owl-next float-end tw-secondary"], 1376 items: 3, 1377 autoWidth: false, 1378 center: true, 1379 URLhashListener: true, 1380 autoplayHoverPause: false, 1381 startPosition: "URLHash", 1382 responsive: { 1383 0: { 1384 items: 1, 1385 }, 1386 992: { 1387 items: 1, 1388 }, 1389 }, 1390 }; 1391 var owlActive = owl.owlCarousel(owlOptions); 1392 $("#sectionNewsDefault .owl-prev").attr("aria-label", "Previous news item").removeAttr("role"); 1393 $("#sectionNewsDefault .owl-next").attr("aria-label", "Next news item").removeAttr("role"); 1394 1395 $(".customizer-content a").click(function (e) { 1396 e.preventDefault(); 1397 }); 1398 1399 let owlHeroQuickLinks = $("#homepage-nav.owl-carousel"); 1400 let owlOptionsHeroQuickLinks = { 1401 items: 4, 1402 nav: true, 1403 loop: false, 1404 rewind: true, 1405 dots: false, 1406 margin: 0, 1407 checkVisible: false, 1408 responsiveBaseElement: "#homepage-nav", 1409 responsiveRefreshRate: "0ms", 1410 navText: ['<i class="fa fa-chevron-left fa-1x" aria-hidden="true"></i>', '<i class="fa fa-chevron-right fa-1x" aria-hidden="true"></i>'], 1411 responsive: { 1412 1280: { 1413 items: 4, 1414 }, 1415 1199: { 1416 margin: 140, 1417 }, 1418 991: { 1419 items: 4, 1420 margin: 90, 1421 }, 1422 767: { 1423 items: 3, 1424 margin: 15, 1425 }, 1426 400: { 1427 items: 2, 1428 margin: 0, 1429 }, 1430 0: { 1431 items: 1, 1432 margin: 0, 1433 autoHeight: true, 1434 autoHeightClass: "owl-height", 1435 }, 1436 }, 1437 }; 1438 var owlActiveHeroQuickLinks = owlHeroQuickLinks.owlCarousel(owlOptionsHeroQuickLinks); 1439 $("#homepage-nav .owl-prev").attr("aria-label", "Previous quick links").removeAttr("role"); 1440 $("#homepage-nav .owl-next").attr("aria-label", "Next quick links").removeAttr("role"); 1441
1442 function markHeroClonesInert() { 1443 $("#homepage-nav .owl-item.cloned").each(function () { 1444 var $clone = $(this); 1445 $clone.attr({ "aria-hidden": "true", inert: "" }); 1446 $clone.find("a").each(function () { 1447 var $a = $(this); 1448 if ($a.is("[href]") && !$a.data("twOrigHref")) { 1449 $a.data("twOrigHref", $a.attr("href")); 1450 } 1451 $a.removeAttr("href") 1452 .attr({ "tabindex": "-1", "aria-hidden": "true", role: "presentation" }); 1453 }); 1454 }); 1455 } 1456 markHeroClonesInert(); 1457 owlHeroQuickLinks.on("translated.owl.carousel refreshed.owl.carousel resized.owl.carousel", markHeroClonesInert); 1458 1459 // jQuery(".homepage-nav").owlCarousel({ 1460 // items:4, 1461 // nav:true, 1462 // dots: false, 1463 // loop:true, 1464 // margin:0, 1465 // checkVisible: false, 1466 // responsiveBaseElement: '.homepage-menu', 1467 // responsiveRefreshRate: '0ms', 1468 // navText:['<i class="fa fa-chevron-left fa-1x"></i>','<i class="fa fa-chevron-right fa-1x"></i>'], 1469 // responsive:{ 1470 // 1280:{ 1471 // items:4 1472 // }, 1473 // 1199:{ 1474 // margin:140 1475 // }, 1476 // 991:{ 1477 // items:4, 1478 // margin:90 1479 // }, 1480 // 767:{ 1481 // items:3, 1482 // margin:15 1483 // }, 1484 // 479:{ 1485 // items:2, 1486 // margin:0 1487 // }, 1488 // 0:{ 1489 // items:1, 1490 // margin:0, 1491 // autoHeight: true, 1492 // autoHeightClass: 'owl-height' 1493 // } 1494 // } 1495 // }); 1496 1497 // Handle search ----------------------------------------------------------------------------------------------------------------------------------------------- 1498 function generateSearchItem(item) { 1499 if (jActiveAdvancedSearch == 0) { 1500 var dom = new DOMParser().parseFromString(item.content, "text/html"); 1501 var content = dom.body.textContent; 1502 content = content.substring(0, 150) + "..."; 1503 } 1504 let html = ""; 1505 html = ` 1506 <div class='col-md-4 mb-3'><h2><a href=" 1507 ${jActiveAdvancedSearch == 1 ? item.url : window.location.protocol + "//" + window.location.host + item.slug} 1508 "> 1509 ${item.title} 1510 </a></h2><div> 1511 ${content || item.content} 1512 </div></div> 1513 `; 1514 return html; 1515 } 1516 1517 if (window.location.pathname.includes("search")) { 1518 const queryString = window.location.search; 1519 const urlParams = new URLSearchParams(queryString); 1520 const q = urlParams.get("q"); 1521 // Load the locations once, on page-load. 1522 if (jActiveAdvancedSearch == 1) { 1523 $(async function () { 1524 const res = await fetch("/search/search.php", { 1525 method: "POST", 1526 headers: { 1527 "Content-Type": "application/json", 1528 }, 1529 body: JSON.stringify({ q: q }), 1530 }); 1531 const data = await res.json(); 1532 1533 let searchContainer = document.getElementById("search-results"); 1534 let searchString = document.getElementById("search-string"); 1535 searchString.innerHTML = q; 1536 let searchArr = []; 1537 // Iterate over each element in the array 1538 for (var i = 0; i < data.length; i++) { 1539 searchContainer.innerHTML += generateSearchItem(data[i]); 1540 searchArr.push(data[i]); 1541 } 1542 if (searchArr.length < 1) { 1543 searchContainer.innerHTML += '<div class="text-center text-warning fs-1">There are no results found</div>'; 1544 } 1545 // console.log(data); 1546 }); 1547 } else { 1548 $(function () { 1549 $.getJSON(window.location.protocol + "//" + window.location.host + `/json/search/search.json?sk=${Date.now()}`) 1550 .done(function (data) { 1551 let searchContainer = document.getElementById("search-results"); 1552 let searchString = document.getElementById("search-string"); 1553 searchString.innerHTML = q; 1554 let searchArr = []; 1555 // Iterate over each element in the array 1556 for (var i = 0; i < data.length; i++) { 1557 var content = data[i].content; 1558 // look for the entry with a matching `code` value 1559 if (data[i].title.toLowerCase().includes(q.toLowerCase())) { 1560 searchContainer.innerHTML += generateSearchItem(data[i]); 1561 searchArr.push(data[i]); 1562 } else { 1563 if (content && content.toLowerCase().includes(q.toLowerCase())) { 1564 searchContainer.innerHTML += generateSearchItem(data[i]); 1565 searchArr.push(data[i]); 1566 } 1567 } 1568 } 1569 if (searchArr.length < 1) { 1570 searchContainer.innerHTML += '<div class="text-center text-warning fs-1">There are no results found</div>'; 1571 } 1572 }) 1573 .fail(function (data) { 1574 console.log("Error loading results â"); 1575 }); 1576 }); 1577 } 1578 } 1579 1580 // Handle Contact Us ----------------------------------------------------------------------------------------------------------------------------------------------------- 1581 // $(".contact-form #submit-btn").click(function (e) { 1582 // e.preventDefault() 1583 // data = $(".contact-form").serialize() 1584 // url = window.location.protocol + "//" + window.location.host 1585 // $.ajax({ 1586 // method: "post", 1587 // url: jurl + "/static/contact", // NEED TO UPDATE 1588 // data: $(".contact-form").serialize() + "&url=" + url, 1589 // success: function (msg) { 1590 // if (msg.message == "Success") { 1591 // $(".contact-success").removeClass("d-none") 1592 // $(".contact-success").html(jcontactSuccessMsg) 1593 // setTimeout(cleanContactUs, 3000) 1594 // } 1595 // }, 1596 // fail: function (err) { 1597 // console.log("Error submitting contact form â") 1598 // } 1599 // }) 1600 // }) 1601 1602 $("form.contact-form").submit(function (e) { 1603 data = $("form.contact-form").serialize(); 1604 url = window.location.protocol + "//" + window.location.host; 1605 $.ajax({ 1606 method: "post", 1607 url: jurl + "/static/contact", // NEED TO UPDATE 1608 data: $("form.contact-form").serialize() + "&url=" + url, 1609 success: function (msg) { 1610 if (msg.message == "Success") { 1611 $(".contact-success").removeClass("d-none"); 1612 $(".contact-success").html(jcontactSuccessMsg); 1613 setTimeout(cleanContactUs, 3000); 1614 } 1615 }, 1616 error: function (xhr, status, error) { 1617 console.log("Error submitting contact form â"); 1618 }, 1619 }); 1620 }); 1621 1622 // Clean inputs and hide success message after 3000ms 1623 function cleanContactUs() { 1624 $("#first").val(""); 1625 $("#last").val(""); 1626 $("#email").val(""); 1627 $("#phone").val(""); 1628 $("#message").val(""); 1629 $(".contact-success").addClass("d-none"); 1630 } 1631 1632 // Handle Quick Subscribe ------------------------------------------------------------------------------------------------------------------------------------------------- 1633 1634 function validateEmail(email) { 1635 var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 1636 return re.test(String(email).toLowerCase()); 1637 } 1638 1639 // Validation regex patterns (same as unified.php) 1640 const nameRegex = /^[A-Za-zÃ-ÃÃ-öø-ÿ' -]{2,50}$/; 1641 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; 1642 const usPhoneRegex = /^\+?1?\s?\(?([2-9][0-9]{2})\)?[-.\s]?[2-9][0-9]{2}
1642[-.\s]?[0-9]{4}$/; 1643 1644 // Validation function for names 1645 function validateName(name) { 1646 return name && name.trim() && nameRegex.test(name.trim()); 1647 } 1648 1649 // Validation function for email (improved) 1650 function validateEmailStrict(email) { 1651 return email && email.trim() && emailRegex.test(email.trim()); 1652 } 1653 1654 // Validation function for US phone 1655 function validateUSPhone(phone) { 1656 return phone && phone.trim() && usPhoneRegex.test(phone.trim()); 1657 } 1658 1659 // Check if unified subscribe is being used 1660 const useUnifiedSubscribe = typeof jUseUnifiedSubscribe !== "undefined" && jUseUnifiedSubscribe; 1661 const isUnifiedDashboard = typeof jMainDashboard !== "undefined" && jMainDashboard === "unified-dashboard"; 1662 1663 // Unified Quick Subscribe Handler (legacy) 1664 async function handleUnifiedQuickSubscribe(firstName, lastName, email, phone) { 1665 try { 1666 // Fetch departments 1667 const departmentsResponse = await fetch("https://api.heygov.com/" + jHeyGovJurisdiction + "/departments"); 1668 const departments = await departmentsResponse.json(); 1669 1670 // Fetch lists 1671 const listsResponse = await fetch("https://heygov-api-main-438038920523.us-central1.run.app/" + jHeyGovJurisdiction + "/lists"); 1672 const lists = await listsResponse.json(); 1673 1674 // Find Agenda and Minutes list 1675 let amlist_id = null; 1676 const filteredLists = []; 1677 1678 lists.forEach((list) => { 1679 if (list.name === "Agenda and Minutes") { 1680 amlist_id = list.id; 1681 } else { 1682 filteredLists.push(list.id); 1683 } 1684 }); 1685 1686 // Build subscribed_to array 1687 const subscribedTo = []; 1688 if (email && email.trim() !== "") { 1689 subscribedTo.push("email"); 1690 } 1691 if (phone && phone.trim() !== "") { 1692 subscribedTo.push("sms"); 1693 } 1694 1695 // Build lists array 1696 const listsData = []; 1697 1698 // Add all departments with Agenda and Minutes list 1699 if (departments.length > 0 && amlist_id) { 1700 listsData.push({ 1701 list_id: amlist_id, 1702 departments: departments.map((dept) => dept.id), 1703 subscribed_to: subscribedTo, 1704 }); 1705 } 1706 1707 // Add all other lists 1708 filteredLists.forEach((listId) => { 1709 listsData.push({ 1710 list_id: listId, 1711 departments: null, 1712 subscribed_to: subscribedTo, 1713 }); 1714 }); 1715 1716 // Convert phone to number with 1 prefix for US 1717 let phoneNumber = null; 1718 if (phone && phone.trim() !== "") { 1719 const phoneDigits = phone.replace(/\D/g, ""); 1720 phoneNumber = parseInt("1" + phoneDigits, 10); 1721 } 1722 1723 // Build final request data - only include non-empty fields 1724 const data = { 1725 lists: listsData, 1726 }; 1727 1728 // Add email if provided 1729 if (email && email.trim() !== "") { 1730 data.email = email; 1731 } 1732 1733 // Add phone_number if provided 1734 if (phoneNumber) { 1735 data.phone_number = phoneNumber; 1736 } 1737 1738 // Add first_name if provided 1739 if (firstName && firstName.trim() !== "") { 1740 data.first_name = firstName; 1741 } 1742 1743 // Add last_name only if provided 1744 if (lastName && lastName.trim() !== "") { 1745 data.last_name = lastName; 1746 } 1747 1748 // Submit to unified API 1749 const response = await fetch("https://api.heygov.com/" + jHeyGovJurisdiction + "/subscribe", { 1750 method: "POST", 1751 headers: { 1752 "Content-Type": "application/json", 1753 }, 1754 body: JSON.stringify(data), 1755 }); 1756 1757 if (!response.ok) { 1758 throw new Error("Server responded with status: " + response.status); 1759 } 1760 1761 return await response.json(); 1762 } catch (error) { 1763 throw error; 1764 } 1765 } 1766 1767 // Auto Lists Quick Subscribe Handler (for unified-dashboard) 1768 async function handleAutoListsSubscribe(firstName, lastName, email, phone) { 1769 try { 1770 // Convert phone to number with 1 prefix for US 1771 let phoneNumber = null; 1772 if (phone && phone.trim() !== "") { 1773 const phoneDigits = phone.replace(/\D/g, ""); 1774 phoneNumber = parseInt("1" + phoneDigits, 10); 1775 } 1776 1777 // Build final request data - only include non-empty fields 1778 const data = { 1779 auto_lists: true, 1780 }; 1781 1782 // Add email if provided 1783 if (email && email.trim() !== "") { 1784 data.email = email; 1785 } 1786 1787 // Add phone_number if provided 1788 if (phoneNumber) { 1789 data.phone_number = phoneNumber; 1790 } 1791
1792 // Add first_name if provided 1793 if (firstName && firstName.trim() !== "") { 1794 data.first_name = firstName; 1795 } 1796 1797 // Add last_name only if provided 1798 if (lastName && lastName.trim() !== "") { 1799 data.last_name = lastName; 1800 } 1801 1802 // Submit to unified API 1803 const response = await fetch("https://heygov-api-main-438038920523.us-central1.run.app/" + jHeyGovJurisdiction + "/lists/subscribe", { 1804 method: "POST", 1805 headers: { 1806 "Content-Type": "application/json", 1807 }, 1808 body: JSON.stringify(data), 1809 }); 1810 1811 // Parse response 1812 const responseText = await response.text(); 1813 let responseData = {}; 1814 try { 1815 responseData = responseText ? JSON.parse(responseText) : {}; 1816 } catch (e) { 1817 // Failed to parse JSON 1818 } 1819 1820 if (!response.ok) { 1821 // Extract error message from response 1822 let errorMessage = "An error occurred while processing your subscription. Please try again later."; 1823 if (responseData && responseData.message) { 1824 errorMessage = responseData.message; 1825 } else if (responseData && responseData.error) { 1826 errorMessage = responseData.error; 1827 } 1828 1829 const error = new Error(errorMessage); 1830 error.responseData = responseData; 1831 throw error; 1832 } 1833 1834 return responseData; 1835 } catch (error) { 1836 throw error; 1837 } 1838 } 1839 1840 $("#sub_phone").mask("999-999-9999", { 1841 placeholder: "___-___-____", 1842 clearIfNotMatch: true, 1843 }); 1844 1845 // Clear error message when user starts typing in name field 1846 $("#sub_name").on("input", function () { 1847 $("#sub_name-error").remove(); 1848 }); 1849 1850 $("#subs_button").click(function (e) { 1851 e.preventDefault(); 1852 1853 let fullName = $("#sub_name").val().trim(); 1854 let nameParts = fullName.split(" "); 1855 let firstName = nameParts[0] || ""; 1856 let lastName = nameParts.length > 1 ? nameParts.slice(1).join(" ") : ""; 1857 let email = $("#sub_email").val(); 1858 let phone = $("#sub_phone").val(); 1859 1860 // Validate that at least first name is provided 1861 if (!firstName) { 1862 $("#sub_name-error").remove(); 1863 $("#sub_name").after('<div id="sub_name-error" style="color: red; font-size: 12px; margin-top: 4px;">Please enter your name</div>'); 1864 $("#sub_name").focus(); 1865 return; 1866 } 1867 1868 // Check if form is valid using HTML5 validation 1869 if ($("#homepage-subscriber-form-main")[0].checkValidity()) { 1870 let btn_html = $("#subs_button").html(); 1871 $("#subs_button + div.alert").remove(); 1872 $("#subs_button").html('<i class="fa fa-spinner fa-spin"></i> Working'); 1873 1874 $("#sub_name").attr("readonly", true); 1875 $("#sub_email").attr("readonly", true); 1876 $("#sub_phone").attr("readonly", true); 1877 1878 // Use unified-dashboard auto lists subscribe 1879 if (isUnifiedDashboard) { 1880 handleAutoListsSubscribe(firstName, lastName, email, phone) 1881 .then((result) => { 1882 // Success 1883 $("#subs_button").addClass("btn-success homepage-subscribed-success font-weight-bold"); 1884 $("#subs_button").html("Successfully Subscribed!"); 1885 1886 setTimeout(function () { 1887 $("#subs_button").removeClass("btn-success"); 1888 $("#subs_button").removeClass("homepage-subscribed-success"); 1889 $("#subs_button").html("Subscribe now"); 1890 }, 6000); 1891 1892 // Clear inputs 1893 $("#sub_name").val(""); 1894 $("#sub_email").val(""); 1895 $("#sub_phone").val(""); 1896 $("#sub_name").attr("readonly", false); 1897 $("#sub_email").attr("readonly", false); 1898 $("#sub_phone").attr("readonly", false); 1899 }) 1900 .catch((error) => { 1901 console.error("Error:", error); 1902 $("#subs_button").addClass("btn-danger homepage-subscribed-danger font-weight-bold"); 1903 $("#subs_button").html("Error Occurred!"); 1904 1905 // Use error message from API if available 1906 const errorMessage = error.message || "An error occurred while processing your subscription. Please try again later."; 1907 1908 $("#homepage-subscriber-form-main").after( 1909 `<div class="row ms-1">
1909<div id="subscribe-error" class="alert alert-danger alert-dismissible" role="alert">${errorMessage}<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></div></div>`, 1910 ); 1911 1912 setTimeout(function () { 1913 $("#subs_button").removeClass("btn-danger"); 1914 $("#subs_button").removeClass("homepage-subscribed-danger"); 1915 $("#subs_button").html("Subscribe now"); 1916 $("#subscribe-error").fadeOut(); 1917 }, 6000); 1918 1919 $("#sub_name").attr("readonly", false); 1920 $("#sub_email").attr("readonly", false); 1921 $("#sub_phone").attr("readonly", false); 1922 }); 1923 } else if (useUnifiedSubscribe) { 1924 // Use old unified subscribe (legacy) 1925 handleUnifiedQuickSubscribe(firstName, lastName, email, phone) 1926 .then((result) => { 1927 // Success 1928 $("#subs_button").addClass("btn-success homepage-subscribed-success font-weight-bold"); 1929 $("#subs_button").html("Successfully Subscribed!"); 1930 1931 setTimeout(function () { 1932 $("#subs_button").removeClass("btn-success"); 1933 $("#subs_button").removeClass("homepage-subscribed-success"); 1934 $("#subs_button").html("Subscribe now"); 1935 }, 6000); 1936 1937 // Clear inputs 1938 $("#sub_name").val(""); 1939 $("#sub_email").val(""); 1940 $("#sub_phone").val(""); 1941 $("#sub_name").attr("readonly", false); 1942 $("#sub_email").attr("readonly", false); 1943 $("#sub_phone").attr("readonly", false); 1944 }) 1945 .catch((error) => { 1946 console.error("Error:", error); 1947 $("#subs_button").addClass("btn-danger homepage-subscribed-danger font-weight-bold"); 1948 $("#subs_button").html("Error Occurred!"); 1949 $("#homepage-subscriber-form-main").after( 1950 `<div class="row ms-1"><div id="subscribe-error" class="alert alert-danger alert-dismissible" role="alert">An error occurred while processing your subscription.<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></div></div>`, 1951 ); 1952 1953 setTimeout(function () { 1954 $("#subs_button").removeClass("btn-danger"); 1955 $("#subs_button").removeClass("homepage-subscribed-danger"); 1956 $("#subs_button").html("Subscribe now"); 1957 $("#subscribe-error").fadeOut(); 1958 }, 6000); 1959 1960 $("#sub_name").attr("readonly", false); 1961 $("#sub_email").attr("readonly", false); 1962 $("#sub_phone").attr("readonly", false); 1963 }); 1964 } else { 1965 // Use legacy subscribe 1966 $.ajax({ 1967 method: "post", 1968 url: jurl + "/subscribers/register?client_id=" + jid, 1969 data: { 1970 name: fullName, 1971 email: email, 1972 phone: phone, 1973 quick: true, 1974 }, 1975 success: function (msg) { 1976 //display success 1977 if (msg.type != "success") { 1978 errrMessage = msg.message; 1979 if (msg && msg.errors && msg.errors.email) { 1980 errrMessage = msg.errors.email[0]; 1981 } 1982 $("#subs_button").addClass("btn-danger homepage-subscribed-danger font-weight-bold"); 1983 $("#subs_button").html("Error Occurred!"); 1984 $("#homepage-subscriber-form-main").after( 1985 `<div class="row ms-1"><div id="subscribe-error" class="alert alert-danger alert-dismissible" role="alert"> ${errrMessage}<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></div></div>`, 1986 ); 1987 //Remove alert, button danger & button text after 3 sec 1988 setTimeout(function () { 1989 $("#subs_button").removeClass("btn-danger"); 1990 $("#subs_button").addClass("subscribe-button"); 1991 $("#subs_button").html("Subscribe now"); 1992 $("#subscribe-error").fadeOut(); 1993 }, 6000); 1994 } else { 1995 if ($("#subs_button").hasClass("homepage-subscribed-danger")) { 1996 $("#subs_button").removeClass("homepage-subscribed-danger"); 1997 } 1998 if ($("#subs_button").hasClass("btn-danger")) { 1999 $("#subs_button").removeClass("btn-danger"); 2000 } 2001 $("#subs_button").addClass("btn-success homepage-subscribed-success font-weight-bold"); 2002 $("#subs_button").html("Successfully Subscribed!"); 2003 // Remove success class after 3 sec 2004 setTimeout(function () { 2005 $("#subs_button").removeClass("btn-success"); 2006 $("#subs_button").addClass("subscribe-button"); 2007 $("#subs_button").html("Subscribe now"); 2008 }, 6000); 2009 } 2010 //clear input values 2011 $("#sub_name").val(""); 2012 $("#sub_email").val(""); 2013 $("#sub_phone").val(""); 2014 $("#sub_name").attr("readonly", false); 2015 $("#sub_email").attr("readonly", false); 2016 $("#sub_phone").attr("readonly", false); 2017 }, 2018 fail: function (jqXHR, textStatus) { 2019 //display error message 2020 $("#subs_button").html("Error"); 2021 2022 //clear input values 2023 $("#sub_name").val(""); 2024 $("#sub_email").val(""); 2025 $("#sub_phone").val(""); 2026 $("#sub_name").attr("readonly", false); 2027 $("#sub_email").attr("readonly", false); 2028 $("#sub_phone").attr("readonly", false); 2029 }, 2030 }); 2031 } 2032 } else { 2033 // Trigger browser validation messages 2034 $("#homepage-subscriber-form-main")[0].reportValidity(); 2035 } 2036 }); 2037 2038 // Hande Subscribe ------------------------------------------------------------------------------------------------------------------------------------------------------------ 2039 2040 // Create meeting categories 2041 function generateMeetingCategories() {
2042 let categoriesHtml = ""; 2043 let categoriesContainer = $("#juniper-meetings-categories"); 2044 let meetingCategories = JSON.parse(jMeetingsSubscribableCategories); 2045 meetingCategories.forEach((category) => { 2046 categoriesHtml += `<div class="sub"> 2047 <input id="juniper-subscriber-agenda-${category.id}" class="juniper-child-checkbox" type="checkbox" name="agenda_terms[]" value="${category.id}"> 2048 <label for="juniper-subscriber-agenda-${category.id}">${category.name}</label><br> 2049 </div>`; 2050 }); 2051 categoriesContainer.html(categoriesHtml); 2052 } 2053 2054 function generateNewsCategories() { 2055 let newsCategoriesHtml = ""; 2056 let newsCategoriesContainer = $("#juniper-news-categories"); 2057 let newsCategories = JSON.parse(jNewsSubscribableCategories); 2058 newsCategories.forEach((category) => { 2059 newsCategoriesHtml += `<div class="sub"> 2060 <input id="juniper-subscriber-news-${category.id}" class="juniper-child-checkbox" type="checkbox" name="post_terms[]" value="${category.id}"> 2061 <label for="juniper-subscriber-news-${category.id}">${category.name}</label><br> 2062 </div>`; 2063 }); 2064 newsCategoriesContainer.html(newsCategoriesHtml); 2065 } 2066 2067 // Invoke generate meeting categories 2068 generateMeetingCategories(); 2069 // Invoke generate news categories 2070 generateNewsCategories(); 2071 2072 // Mask for Phone 2073 $("#juniper-subscriber-phone").mask("(999) 999-9999"); 2074 2075 // Checkbox 2076 $(".juniper-parent-checkbox") 2077 .click(function () { 2078 $(this).parent().find(".juniper-child-checkbox").prop("disabled", !this.checked).prop("checked", this.checked); 2079 }) 2080 .trigger("click"); 2081 2082 // Phone required when sms checked 2083 $("#juniper-subscriber-receive-text-message") 2084 .click(function () { 2085 $("#juniper-subscriber-phone, #juniper-subscriber-carrier").prop("required", this.checked); 2086 }) 2087 .trigger("click"); 2088 2089 // Handle subscribe now button 2090 $("body").on("submit", "#juniper-subscriber-form", function () { 2091 $.ajax({ 2092 type: "POST", 2093 url: jurl + "/subscribers/register?client_id=" + jid, 2094 data: $(this).serialize(), 2095 success: function (data) { 2096 if (data.type == "success") { 2097 $("#juniper-subscribe-complete").removeClass("d-none"); 2098 } 2099 if (data.type == "error") { 2100 let text = ""; 2101 for (const [key, value] of Object.entries(data.errors)) { 2102 text += `<p>${value}</p>`; 2103 } 2104 $("#juniper-subscribe-error").removeClass("d-none"); 2105 $("#juniper-subscribe-error").html(text); 2106 } 2107 }, 2108 fail: function (error) { 2109 console.log("Error submiting subscriber form â"); 2110 }, 2111 }); 2112 2113 return true; 2114 }); 2115 2116 // Handle unsubscribe button 2117 $("body").on("click", "#twd-unsubscribe-btn", function () { 2118 if (confirm("Are you sure?")) { 2119 $.ajax({ 2120 type: "GET", 2121 url: $(this).attr("href"), 2122 dataType: "html", 2123 }).done(function (date) { 2124 $("#twd-subscribe-complete").html(date); 2125 $("#twd-subscriber-submit").val("Subscribe"); 2126 $("#twd-subscriber-form").slideDown(); 2127 }); 2128 } 2129 return false; 2130 }); 2131}); 2132 2133function waitForElement(selector) { 2134 return new Promise((resolve) => { 2135 if (document.querySelector(selector)) { 2136 return resolve(document.querySelector(selector)); 2137 } 2138 2139 const observer = new MutationObserver((mutations) => { 2140 if (document.querySelector(selector)) { 2141 observer.disconnect(); 2142 resolve(document.querySelector(selector)); 2143 } 2144 }); 2145 2146 observer.observe(document.body, { 2147 childList: true, 2148 subtree: true, 2149 }); 2150 }); 2151} 2152 2153/*****************************************/ 2154/*****************************************/ 2155/***********CALENDAR 2019*****************/ 2156/*****************************************/ 2157/*****************************************/ 2158jQuery(document).ready(function () { 2159 (async () => { 2160 $('body [data-bs-toggle="tooltip"]').tooltip("enable"); 2161 let eventsList2019, eventsListContainer2019, headMonth2019, monthAndYear2019, calendarBodyTable2019; 2162 2163 eventsList2019 = waitForElement("#events-calendar-2019-list"); 2164 eventsListContainer2019 = waitForElement("#events-calendar-2019-list-container"); 2165 headMonth2019 = waitForElement("#thead-month-2019"); 2166 monthAndYear2019 = waitForElement("#monthAndYearTitle2019"); 2167 calendarBodyTable2019 = waitForElement("#calendar-2019-body"); 2168 2169 let eventsListEmpty = "No Events Today."; 2170 2171 let dayNames2019 = ["S", "M", "T", "W", "T", "F", "S"]; 2172 2173 let dayNames2019Content = "<tr>"; 2174 dayNames2019.forEach((name) => { 2175 dayNames2019Content += `<th class="text-center text-capitalize" data-days="${name}">${name}</th>`; 2176 }); 2177 dayNames2019Content += "</tr>"; 2178 2179 headMonth2019.then((el) => (el.innerHTML = dayNames2019Content)); 2180 2181 if ((jActiveHomepageSections && jActiveHomepageSections.includes("Events-calendar-2019") && window.location.pathname == "/") || window.location.pathname.includes("customizer")) { 2182 calendarBodyTable2019.then(async () => { 2183 events = await getEvents(eventCategory); 2184 showCalendar2019(currentMonth, currentYear); 2185 // setEventsListDate(today) 2186 generateDayEvents2019List(today, todayMonth, todayYear); 2187 }); 2188 } 2189 2190 // Show Calendar 2019 2191 async function showCalendar2019(month, year) { 2192 let firstDay = new Date(year, month).getDay(); 2193 let tbl = await waitForElement("#calendar-2019-body"); 2194 tbl.innerHTML = ""; 2195 monthAndYear2019.then((el) => (el.innerHTML = months[month] + ", " + year)); 2196 // Create all calendar items 2197 2198 let calendarRows = getCalendarRows(); 2199 // console.log(calendarRows) 2200 // Create all calendar items 2201 2202 calendarRows.forEach((calendarRow) => { 2203 var row = document.createElement("tr"); 2204 calendarRow.forEach((day) => { 2205 let cell = document.createElement("td"); 2206 2207 if (isDateOfPreviousMonth(day)) { 2208 cell.classList.add("previous-month"); 2209 } 2210 2211 if (isDateOfNextMonth(day)) { 2212 cell.classList.add("next-month"); 2213 } 2214 2215 // check if calendar date has events 2216 let dayEvents = getEventOnDay(day); 2217 2218 let div = document.createElement("div"); 2219 div.className = "text-center"; 2220 2221 let todayCloneForCalendarCell = new Date(today); 2222 todayCloneForCalendarCell.setHours(0, 0, 0, 0); 2223 if (todayCloneForCalendarCell.getTime() === day.getTime()) { 2224 cell.classList.add("tw-calendar-day-active"); 2225 } 2226 2227 if (dayEvents.length) { 2228 cell.classList.add("has-events"); 2229 div.classList.add("cursor-pointer"); 2230 div.innerHTML = `<a href="${jEventsPage}/?${dayEvents[0]._isMeeting ? "heygov-meeting" : "heygov-event"}=${dayEvents[0]["pid"]}" class="d-flex justify-content-center text-decoration-none">${day.getDate()}</a>`; 2231 } else { 2232 div.innerText = day.getDate(); 2233 } 2234 2235 cell.appendChild(div); 2236 2237 cell.setAttribute("data-date", day); 2238 cell.setAttribute("data-month", day.getMonth() + 1); 2239 cell.setAttribute("data-year", day.getFullYear()); 2240 cell.setAttribute("data-month_name", months[day.getMonth()]); 2241 2242 row.appendChild(cell); 2243 }); 2244 if (tbl) { 2245 tbl.appendChild(row); 2246 } 2247 }); 2248 } 2249 2250 // Generate list of day events 2251 async function generateDayEvents2019List(day, month, year) { 2252 let dateEnd = new Date(day); 2253 dateEnd.setFullYear(dateEnd.getFullYear() + 2); 2254 2255 let args = { 2256 limit: 6, 2257 date_start: formatDate(day), 2258 date_end: formatDate(dateEnd), 2259 }; 2260 2261 let eventsFor2019List = await getEvents(eventCategory, args); 2262 let events2019List = ""; 2263 2264 eventsFor2019List.forEach((e, index) => { 2265 let startsAt = new Date(
2266 new Date(e.starts_at).toLocaleString("en", { 2267 timeZone: timezone, 2268 }), 2269 ); 2270 2271 let dateStart = startsAt.toLocaleString("en-US", { month: "long" }) + " " + startsAt.getDate() + ", " + startsAt.getFullYear(); 2272 2273 let eventParam = e._isMeeting ? "heygov-meeting" : "heygov-event"; 2274 let isCanceled = e.status === "canceled"; 2275 let hasImage = !!(e.image_path || e.image_url || jeventDefaultFeaturedImage); 2276 // No thumbnail to stamp: show a pill badge above the title instead 2277 let canceledBadge = isCanceled && !hasImage ? '<span class="tw-event-canceled-badge"><i class="fa-solid fa-ban" aria-hidden="true"></i> Canceled Event</span>' : ""; 2278 2279 events2019List += `<div class="single-homepage-event d-flex pb-3"> 2280 <article class="d-flex flex-grow-1${isCanceled ? " event-canceled" : ""}" aria-labelledby="homepage-event-${e.pid}-title"> 2281 <div class="d-flex flex-grow-1 flex-column event-item"> 2282 <div class="article-thumbnail"> 2283 ${ 2284 hasImage 2285 ? `<a href="${jEventsPage}/?${eventParam}=${e.pid}"> 2286 <img src="${e.image_path || e.image_url ? (e.image_path ? jHeyGovStorage + e.image_path : e.image_url) : jeventDefaultFeaturedImage}" class="img-responsive" data-uw-rm-alt-original="" alt="text" data-uw-rm-alt="BE"> 2287 2288 </a>` 2289 : "" 2290 } 2291 </div> 2292 2293 <div class="event-item-card d-flex flex-grow-1 flex-column"> 2294 <div class="event-date"> 2295 ${dateStart} 2296 </div> 2297 <div class=""> 2298 ${canceledBadge} 2299 <a href="${jEventsPage}/?${eventParam}=${e.pid}"> 2300 <h4 id="homepage-event-${e.pid}-title">${isCanceled && !canceledBadge ? '<span class="visually-hidden">Canceled: </span>' : ""}${e.name}</h4> 2301 </a> 2302 <div class="entry-content"> 2303 <p> 2304 ${(() => { 2305 if (!e.description) return ""; 2306 // Remove HTML links <a>...</a> but keep the text 2307 const descWithoutLinks = e.description 2308 .replace(/<a[^>]*>([^<]*)<\/a>/gi, "$1") // Remove <a> tags but keep inner text 2309 .replace(/<[^>]*>/g, "") // Remove remaining HTML so malformed tags cannot break the cards 2310 .replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1") // Markdown links [text](url) -> text 2311 .replace(/https?:\/\/[^\s]+/g, ""); // Plain URLs 2312 if (descWithoutLinks.length > 100) { 2313 return descWithoutLinks.slice(0, 100).replace(/\p{P}\s*$/u, "") + "..."; 2314 } 2315 return descWithoutLinks; 2316 })()} 2317 2318 </p> 2319 </div> 2320 ${!isCanceled && e.description && e.description.length > 100 ? `<div class="tw-read-more mt-auto"><a class="read-more-link" href="${jEventsPage}/?${eventParam}=${e.pid}">Read More</a></div>` : ""} 2321 </div> 2322 </div> 2323 </div> 2324 </article> 2325 </div>`; 2326 }); 2327 2328 if (events2019List == "") { 2329 events2019List = eventsListEmpty; 2330 } 2331 eventsListContainer2019.then((el) => (el.innerHTML = events2019List)); 2332 2333 // Fix for tooltip to go allways left 2334 const tooltipLinks = document.querySelectorAll('#tw-quick-links a[data-bs-toggle="tooltip"]'); 2335 tooltipLinks.forEach((link) => { 2336 const tooltipInstance = bootstrap.Tooltip.getInstance(link); 2337 if (tooltipInstance) { 2338 tooltipInstance.dispose(); 2339 } 2340 link.setAttribute("data-bs-placement", "left"); 2341 new bootstrap.Tooltip(link); 2342 }); 2343 } 2344 })(); 2345 document.querySelectorAll("#main-menu .dropdown-menu .dropdown-toggle").forEach((item) => { 2346 item.addEventListener("click", function (event) { 2347 event.stopPropagation(); // Prevent Bootstrap from closing the entire dropdown 2348 let submenu = this.nextElementSibling; 2349 if (submenu) { 2350 submenu.classList.toggle("show"); 2351 } 2352 }); 2353 }); 2354 2355 // Close all submenus when clicking outside 2356 document.addEventListener("click", function () { 2357 document.querySelectorAll("#main-menu .dropdown-menu .show").forEach((submenu) => { 2358 submenu.classList.remove("show"); 2359 }); 2360 }); 2361});
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.