PageSourceSearch

https://kohlervillagewi.gov/theme.js?v=9deefded

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.