PageSourceSearch

https://cherryvaleusa.com/theme.js?v=9deefded

js cherryvaleusa.com collected 2026-10-02 18:06:51 UTC 89,003 bytes, 2,361 lines download raw bytes

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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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 &amp; 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.