PageSourceSearch

https://catalinaconservancy.org/wp-content/themes/catalinaisland/js/weatheralert.js?ver=1790910581

js catalinaconservancy.org collected 2026-10-02 04:00:39 UTC 8,454 bytes, 243 lines download raw bytes

1// We’ll request alerts for CAZ087, CAZ287 and PZZ655
2window.addEventListener('DOMContentLoaded', () => {
3  const zoneList = ['CAZ087', 'CAZ287', 'PZZ655'];
4
5  // Build an array of fetch promises, one per zone
6  const fetchPromises = zoneList.map((zoneId) => {
7    const apiUrl = `https://api.weather.gov/alerts/active?zone=${zoneId}`;
8    return fetch(apiUrl)
9      .then((response) => {
10        if (!response.ok) {
11          throw new Error(`Network error: ${response.status} (zone: ${zoneId})`);
12        }
13        return response.json();
14      })
15      .catch((err) => {
16        console.error(`Error fetching zone ${zoneId}:`, err);
17        // Return a fallback structure so Promise.all doesn't fail entirely
18        return { features: [] };
19      });
20  });
21
22  // Wait for all fetches to complete
23  Promise.all(fetchPromises)
24    .then((results) => {
25      // Combine 'features' from each zone’s result
26      let allFeatures = [];
27      results.forEach((zoneData) => {
28        if (zoneData.features) {
29          allFeatures = allFeatures.concat(zoneData.features);
30        }
31      });
32
33      // Filter out test alerts more robustly
34      allFeatures = allFeatures.filter((feature) => {
35        const { messageType, event, headline, description } = feature.properties;
36
37        // Convert to lowercase just once for simpler checks
38        const lowerMessageType = (messageType || '').toLowerCase();
39        const lowerEvent = (event || '').toLowerCase();
40        const lowerHeadline = (headline || '').toLowerCase();
41        const lowerDescription = (description || '').toLowerCase();
42
43        // If messageType is 'Test'
44        if (lowerMessageType === 'test') {
45          return false;
46        }
47
48        // If event, headline or description contains 'test'
49        if (
50          lowerEvent.includes('test') ||
51          lowerHeadline.includes('test') ||
52          lowerDescription.includes('test')
53        ) {
54          return false;
55        }
56
57        return true;
58      });
59
60      // If no real alerts remain, bail out
61      if (allFeatures.length === 0) {
62        console.log('No active alerts for either zone.');
63        return;
64      }
65
66      // Convert raw features into simplified alert objects.
67      // For each alert, check if it has PZZ655 in its affectedZones.
68      const simplifiedAlerts = allFeatures.map((feature) => {
69        const props = feature.properties;
70        // affectedZones may be an array of URLs, so we check if any include PZZ655.
71        const affectedZones = props.affectedZones || [];
72        const isPZZ655 = affectedZones.some((zoneUrl) => zoneUrl.includes('PZZ655'));
73        const alertName = props.event || props.headline || 'Weather Alert';
74
75        // Potential date fields
76        const startTime = props.start || props.effective || props.onset;
77        const endTime = props.end || props.expires || props.ends;
78        const apStart = startTime ? formatDateApStyle(startTime) : '';
79        const apEnd = endTime ? formatDateApStyle(endTime) : '';
80
81        let dateText = '';
82        if (apStart || apEnd) {
83          dateText = `issued ${apStart}`;
84          if (apEnd) {
85            dateText += ` until ${apEnd}`;
86          }
87        }
88
89        return {
90          shortName: alertName,
91          dateString: dateText,
92          isPZZ655: isPZZ655
93        };
94      });
95
96      // Now that we have the merged list, create the carousel
97      createAlertCarousel(simplifiedAlerts);
98    })
99    .catch((error) => {
100      console.error('Error fetching alerts for multiple zones:', error);
101    });
102});
103
104/**
105 * Return the PNG file URL based on the alert name
106 * (Adjust paths/filenames to your actual icons)
107 */
108function getIconURLForAlert(alertName) {
109  const lower = alertName.toLowerCase();
110
111  if (lower.includes('marine')) {
112    return 'https://catalinaconservancy.org/wp-content/uploads/2025/01/sailboat-solid.svg';
113  } else if (lower.includes('wind')) {
114    return 'https://catalinaconservancy.org/wp-content/uploads/2025/01/wind-solid1.svg';
115  } else if (
116    lower.includes('cold') ||
117    lower.includes('freeze') ||
118    lower.includes('frost')
119  ) {
120    return 'https://catalinaconservancy.org/wp-content/uploads/2025/01/temperature-low-solid.svg';
121  } else if (lower.includes('fog')) {
122    return 'https://catalinaconservancy.org/wp-content/uploads/2025/01/fog-solid.svg';
123  } else if (lower.includes('winter')) {
124    return 'https://catalinaconservancy.org/wp-content/uploads/2025/01/snowflake-regular.svg';
125  } else if (lower.includes('red flag')) {
126    return 'https://catalinaconservancy.org/wp-content/uploads/2025/01/fire-solid1.svg';
127  } else if (lower.includes('flood')) {
128    return 'https://catalinaconservancy.org/wp-content/uploads/2025/01/water-solid.svg';
129  } else if (lower.includes('thunderstorm')) {
130    return 'https://catalinaconservancy.org/wp-content/uploads/2025/01/cloud-bolt-solid.svg';
131  } else if (lower.includes('craft')) {
132    return 'https://catalinaconservancy.org/wp-content/uploads/2025/01/sailboat-solid.svg';
133  }
134  // Default icon
135  return 'https://catalinaconservancy.org/wp-content/uploads/2025/01/triangle-exclamation-solid1.svg';
136}
137
138/**
139 * Create one bar for all alerts, rotating if multiple
140 */
141function createAlertCarousel(alertsArray) {
142  const appDiv = document.getElementById('nws-app');
143  if (!appDiv) return;
144
145  const bar = document.createElement('div');
146  bar.className = 'nws-alert-bar';
147
148  const contentDiv = document.createElement('div');
149  contentDiv.className = 'nws-alert-content';
150
151  // PNG icon (instead of Font Awesome)
152  const iconEl = document.createElement('img');
153  iconEl.className = 'alert-icon';
154
155  const alertParagraph = document.createElement('p');
156  alertParagraph.className = 'alert-text';
157
158  const alertAnchor = document.createElement('a');
159  alertAnchor.className = 'alert-link';
160  // Default link; it will be updated based on the alert type.
161  alertAnchor.href =
162    'https://forecast.weather.gov/MapClick.php?CityName=Avalon&state=CA&site=LOX&textField1=33.3428&textField2=-118.327&e=1';
163  alertAnchor.target = '_blank';
164  alertAnchor.rel = 'noopener noreferrer';
165
166  const dateSpan = document.createElement('span');
167  dateSpan.className = 'date-span';
168
169  alertParagraph.appendChild(alertAnchor);
170  alertParagraph.appendChild(dateSpan);
171
172  const closeBtn = document.createElement('button');
173  closeBtn.className = 'nws-alert-close';
174  closeBtn.innerHTML = '×';
175  closeBtn.addEventListener('click', () => {
176    bar.remove();
177    if (window.alertInterval) {
178      clearInterval(window.alertInterval);
179    }
180  });
181
182  contentDiv.appendChild(iconEl);
183  contentDiv.appendChild(alertParagraph);
184  contentDiv.appendChild(closeBtn);
185  bar.appendChild(contentDiv);
186  appDiv.insertBefore(bar, appDiv.firstChild);
187
188  let currentIndex = 0;
189  const totalAlerts = alertsArray.length;
190
191  function showAlert(index) {
192    const alertObj = alertsArray[index];
193    iconEl.src = getIconURLForAlert(alertObj.shortName);
194    alertAnchor.textContent = alertObj.shortName;
195    dateSpan.textContent = alertObj.dateString || '';
196
197    // If the alert is from PZZ655, use the custom URL.
198    if (alertObj.isPZZ655) {
199      alertAnchor.href =
200        'https://forecast.weather.gov/MapClick.php?lon=-118.32097917792319&lat=33.34556497647978';
201    } else {
202      alertAnchor.href =
203        'https://forecast.weather.gov/MapClick.php?CityName=Avalon&state=CA&site=LOX&textField1=33.3428&textField2=-118.327&e=1';
204    }
205  }
206
207  // Show the first alert
208  showAlert(currentIndex);
209
210  // Rotate every 5 seconds if multiple alerts
211  if (totalAlerts > 1) {
212    window.alertInterval = setInterval(() => {
213      currentIndex = (currentIndex + 1) % totalAlerts;
214      showAlert(currentIndex);
215    }, 5000);
216  }
217}
218
219/**
220 * Convert an ISO date/time to AP style (e.g. "Jan. 7 at 3 a.m.")
221 */
222function formatDateApStyle(isoString) {
223  const date = new Date(isoString);
224  if (isNaN(date.getTime())) return '';
225
226  const months = [
227    'Jan.', 'Feb.', 'March', 'April', 'May', 'June',
228    'July', 'Aug.', 'Sept.', 'Oct.', 'Nov.', 'Dec.'
229  ];
230  const month = date.getMonth();
231  const day = date.getDate();
232
233  let hours = date.getHours();
234  const minutes = date.getMinutes();
235  const isPM = hours >= 12;
236  const ampm = isPM ? 'p.m.' : 'a.m.';
237  hours = hours % 12 || 12;
238  const minutesFormatted =
239    minutes > 0 ? `:${String(minutes).padStart(2, '0')}` : '';
240
241  const timeString = `${hours}${minutesFormatted} ${ampm}`;
242  return `${months[month]} ${day} at ${timeString}`;
243}

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.