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