PageSourceSearch

https://sitio.laquebuenaatlanta.com/js/weather-news.js?v=14

js laquebuenaatlanta.com collected 2026-10-07 03:39:59 UTC 22,869 bytes, 507 lines download raw bytes

1(function () {
2    'use strict';
3
4    // Weather config
5    const WEATHER_CONFIG = {
6        API_BASE: 'https://api.weather.gov',
7        STATION: 'KATL',
8        LAT: 33.748547,
9        LON: -84.391502,
10        GRID_OFFICE: 'FFC',
11        GRID_X: 52,
12        GRID_Y: 88,
13        REFRESH_INTERVAL: 600000, // 10 min
14        FORECAST_URL: 'https://forecast.weather.gov/MapClick.php?lat=33.748547&lon=-84.391502'
15    };
16
17    const NEWS_CONFIG = {
18        CORS_PROXIES: [
19            'https://api.codetabs.com/v1/proxy?quest=',
20            'https://corsproxy.io/?'
21        ],
22        // allorigins /get endpoint (JSON wrapper with proper CORS headers)
23        ALLORIGINS_GET: 'https://api.allorigins.win/get?url=',
24        BBC_MUNDO_RSS: 'https://feeds.bbci.co.uk/mundo/rss.xml',
25        BBC_IMG_SIZE: 640,
26        REFRESH_INTERVAL: 300000 // 5 min
27    };
28
29    // Translation dictionary for weather conditions
30    const WEATHER_ES = {
31        'Sunny': 'Soleado', 'Clear': 'Despejado', 'Mostly Clear': 'Mayormente Despejado',
32        'Partly Cloudy': 'Parcialmente Nublado', 'Mostly Cloudy': 'Mayormente Nublado',
33        'Cloudy': 'Nublado', 'Overcast': 'Cielo Cubierto',
34        'Fog': 'Niebla', 'Haze': 'Neblina', 'Mist': 'Bruma',
35        'Rain': 'Lluvia', 'Light Rain': 'Lluvia Ligera', 'Heavy Rain': 'Lluvia Fuerte',
36        'Showers': 'Chubascos', 'Drizzle': 'Llovizna',
37        'Thunderstorms': 'Tormentas', 'Severe Thunderstorms': 'Tormentas Severas',
38        'Snow': 'Nieve', 'Light Snow': 'Nieve Ligera', 'Heavy Snow': 'Nevada Fuerte',
39        'Sleet': 'Aguanieve', 'Freezing Rain': 'Lluvia Helada',
40        'Windy': 'Ventoso', 'Breezy': 'Ventoso',
41        'Hot': 'Caluroso', 'Cold': 'Frío',
42        'Chance Showers': 'Prob. Chubascos', 'Slight Chance Showers': 'Baja Prob. Chubascos',
43        'Showers Likely': 'Prob. Alta de Chubascos',
44        'Chance Rain': 'Prob. Lluvia', 'Rain Likely': 'Prob. Alta de Lluvia',
45        'Chance Thunderstorms': 'Prob. Tormentas',
46        'Partly Sunny': 'Parcialmente Soleado', 'Mostly Sunny': 'Mayormente Soleado',
47        'Cold Weather Advisory': 'Aviso de Clima Frío'
48    };
49
50    const WEATHER_ICONS = {
51        'sunny': 'fa-sun', 'clear': 'fa-moon', 'mostly clear': 'fa-moon',
52        'partly cloudy': 'fa-cloud-sun', 'mostly cloudy': 'fa-cloud',
53        'cloudy': 'fa-cloud', 'overcast': 'fa-cloud',
54        'fog': 'fa-smog', 'haze': 'fa-smog', 'mist': 'fa-smog',
55        'rain': 'fa-cloud-rain', 'light rain': 'fa-cloud-rain',
56        'heavy rain': 'fa-cloud-showers-heavy', 'showers': 'fa-cloud-showers-heavy',
57        'drizzle': 'fa-cloud-rain',
58        'thunderstorms': 'fa-bolt', 'severe thunderstorms': 'fa-bolt',
59        'snow': 'fa-snowflake', 'light snow': 'fa-snowflake',
60        'sleet': 'fa-cloud-meatball', 'freezing rain': 'fa-icicles',
61        'windy': 'fa-wind', 'breezy': 'fa-wind',
62        'hot': 'fa-temperature-high', 'cold': 'fa-temperature-low'
63    };
64
65    const DAY_NAMES_ES = {
66        'Monday': 'Lun', 'Tuesday': 'Mar', 'Wednesday': 'Mié',
67        'Thursday': 'Jue', 'Friday': 'Vie', 'Saturday': 'Sáb', 'Sunday': 'Dom',
68        'Today': 'Hoy', 'Tonight': 'Noche', 'Overnight': 'Noche',
69        'This Afternoon': 'Tarde', 'This Evening': 'Noche'
70    };
71
72    document.addEventListener('DOMContentLoaded', function () {
73        loadWeather();
74        setInterval(loadWeather, WEATHER_CONFIG.REFRESH_INTERVAL);
75        loadNews();
76        setInterval(loadNews, NEWS_CONFIG.REFRESH_INTERVAL);
77
78        const newsRefreshBtn = document.getElementById('newsRefreshBtn');
79        if (newsRefreshBtn) {
80            newsRefreshBtn.addEventListener('click', function () {
81                this.classList.add('spinning');
82                loadNews().finally(() => {
83                    setTimeout(() => this.classList.remove('spinning'), 600);
84                });
85            });
86        }
87    });
88
89    // Load weather
90    async function loadWeather() {
91        try {
92            // Fetch current conditions
93            const currentRes = await fetch(
94                `${WEATHER_CONFIG.API_BASE}/stations/${WEATHER_CONFIG.STATION}/observations/latest`,
95                { headers: { 'Accept': 'application/geo+json', 'User-Agent': '
95LaQueBuena101.9FM' } }
96            );
97
98            if (currentRes.ok) {
99                const currentData = await currentRes.json();
100                updateCurrentWeather(currentData);
101            }
102        } catch (err) {
103            console.warn('NWS current weather failed:', err);
104            setFallbackWeather();
105        }
106
107        try {
108            // Fetch forecast
109            const forecastRes = await fetch(
110                `${WEATHER_CONFIG.API_BASE}/gridpoints/${WEATHER_CONFIG.GRID_OFFICE}/${WEATHER_CONFIG.GRID_X},${WEATHER_CONFIG.GRID_Y}/forecast`,
111                { headers: { 'Accept': 'application/geo+json', 'User-Agent': 'LaQueBuena101.9FM' } }
112            );
113
114            if (forecastRes.ok) {
115                const forecastData = await forecastRes.json();
116                updateForecast(forecastData);
117                checkAlerts(forecastData);
118            }
119        } catch (err) {
120            console.warn('NWS forecast failed:', err);
121        }
122
123        // Check for active alerts
124        try {
125            const alertsRes = await fetch(
126                `${WEATHER_CONFIG.API_BASE}/alerts/active?point=${WEATHER_CONFIG.LAT},${WEATHER_CONFIG.LON}`,
127                { headers: { 'Accept': 'application/geo+json', 'User-Agent': 'LaQueBuena101.9FM' } }
128            );
129            if (alertsRes.ok) {
130                const alertsData = await alertsRes.json();
131                updateAlerts(alertsData);
132            }
133        } catch (err) {
134            console.warn('NWS alerts failed:', err);
135        }
136    }
137
138    function updateCurrentWeather(data) {
139        const props = data.properties;
140        if (!props) return;
141
142        const tempC = props.temperature?.value;
143        const tempF = tempC != null ? Math.round((tempC * 9 / 5) + 32) : '--';
144        const desc = props.textDescription || 'N/A';
145        const humidity = props.relativeHumidity?.value != null ? Math.round(props.relativeHumidity.value) : '--';
146        const windSpeedKmh = props.windSpeed?.value;
147        const windMph = windSpeedKmh != null ? Math.round(windSpeedKmh * 0.621371) : '--';
148        const windChillC = props.windChill?.value;
149        const feelsF = windChillC != null ? Math.round((windChillC * 9 / 5) + 32) : tempF;
150
151        const el = (id) => document.getElementById(id);
152
153        if (el('weatherTemp')) el('weatherTemp').textContent = tempF;
154        if (el('weatherDesc')) el('weatherDesc').textContent = translateWeather(desc);
155        if (el('weatherHumidity')) el('weatherHumidity').textContent = `${humidity}%`;
156        if (el('weatherWind')) el('weatherWind').textContent = `${windMph} mph`;
157        if (el('weatherFeels')) el('weatherFeels').textContent = `${feelsF}°F`;
158
159        // Update icon
160        const iconEl = el('weatherIconMain');
161        if (iconEl) {
162            const iconClass = getWeatherIcon(desc);
163            iconEl.className = `fas ${iconClass} weather-icon-main`;
164
165            // Color based on temp
166            if (tempF !== '--') {
167                if (tempF >= 85) iconEl.style.color = '#ff6b35';
168                else if (tempF >= 70) iconEl.style.color = '#FFD700';
169                else if (tempF >= 50) iconEl.style.color = '#4facfe';
170                else if (tempF >= 32) iconEl.style.color = '#82c8ff';
171                else iconEl.style.color = '#a0d2ff';
172            }
173        }
174    }
175
176    function updateForecast(data) {
177        const periods = data.properties?.periods;
178        if (!periods || periods.length === 0) return;
179
180        const forecastEl = document.getElementById('weatherForecast');
181        if (!forecastEl) return;
182
183        let html = '';
184        const count = Math.min(5, periods.length);
185
186        for (let i = 0; i < count; i++) {
187            const p = periods[i];
188            const dayName = translateDay(p.name);
189            const icon = getWeatherIcon(p.shortForecast);
190            const temp = p.temperature;
191            const isNight = p.isDaytime === false;
192
193            html += `
194                <div class="weather-fc-item">
195                    <span class="weather-fc-day">${dayName}</span>
196                    <i class="fas ${icon}
196 weather-fc-icon" style="color: ${isNight ? '#82c8ff' : '#FFD700'}"></i>
197                    <span class="weather-fc-temp">${temp}°</span>
198                </div>`;
199        }
200
201        forecastEl.innerHTML = html;
202    }
203
204    function updateAlerts(data) {
205        const features = data.features;
206        const alertEl = document.getElementById('weatherAlert');
207        const alertText = document.getElementById('weatherAlertText');
208        if (!alertEl || !alertText) return;
209
210        if (features && features.length > 0) {
211            const alert = features[0].properties;
212            const headline = alert.headline || alert.event || 'Alerta Meteorológica';
213            const translated = translateWeather(headline);
214            alertText.textContent = translated;
215            alertEl.style.display = 'flex';
216        }
217    }
218
219    function checkAlerts(data) {
220        // Check if first period has hazard info
221        const periods = data.properties?.periods;
222        if (!periods || periods.length === 0) return;
223
224        const first = periods[0];
225        if (first.detailedForecast && first.detailedForecast.toLowerCase().includes('advisory')) {
226            const alertEl = document.getElementById('weatherAlert');
227            const alertText = document.getElementById('weatherAlertText');
228            if (alertEl && alertText && alertEl.style.display === 'none') {
229                alertText.textContent = 'Aviso meteorológico activo para el área de Atlanta';
230                alertEl.style.display = 'flex';
231            }
232        }
233    }
234
235    function setFallbackWeather() {
236        // Use scraped data from NWS as static fallback
237        const el = (id) => document.getElementById(id);
238        if (el('weatherTemp')) el('weatherTemp').textContent = '30';
239        if (el('weatherDesc')) el('weatherDesc').textContent = 'Parcialmente Nublado';
240        if (el('weatherHumidity')) el('weatherHumidity').textContent = '51%';
241        if (el('weatherWind')) el('weatherWind').textContent = '17 mph NW';
242        if (el('weatherFeels')) el('weatherFeels').textContent = '18°F';
243
244        const alertEl = document.getElementById('weatherAlert');
245        const alertText = document.getElementById('weatherAlertText');
246        if (alertEl && alertText) {
247            alertText.textContent = 'Aviso de Clima Frío hasta las 10:00 AM';
248            alertEl.style.display = 'flex';
249        }
250
251        // Static forecast
252        const forecastEl = document.getElementById('weatherForecast');
253        if (forecastEl) {
254            forecastEl.innerHTML = `
255                <div class="weather-fc-item"><span class="weather-fc-day">Noche</span><i class="fas fa-cloud-moon weather-fc-icon" style="color:#82c8ff"></i><span class="weather-fc-temp">25°</span></div>
256                <div class="weather-fc-item"><span class="weather-fc-day">Lun</span><i class="fas fa-sun weather-fc-icon" style="color:#FFD700"></i><span class="weather-fc-temp">41°</span></div>
257                <div class="weather-fc-item"><span class="weather-fc-day">Mar</span><i class="fas fa-sun weather-fc-icon" style="color:#FFD700"></i><span class="weather-fc-temp">53°</span></div>
258                <div class="weather-fc-item"><span class="weather-fc-day">Mié</span><i class="fas fa-cloud weather-fc-icon" style="color:#a0a0a0"></i><span class="weather-fc-temp">61°</span></div>
259                <div class="weather-fc-item"><span class="weather-fc-day">Jue</span><i class="fas fa-cloud-showers-heavy weather-fc-icon" style="color:#4facfe"></i><span class="weather-fc-temp">66°</span></div>`;
260        }
261    }
262
263    function translateWeather(text) {
264        for (const [en, es] of Object.entries(WEATHER_ES)) {
265            if (text.toLowerCase().includes(en.toLowerCase())) {
266                return es;
267            }
268        }
269        return text;
270    }
271
272    function translateDay(name) {
273        // Remove "Night" suffix for translation
274        for (const [en, es] of Object.entries(DAY_NAMES_ES)) {
275            if (name.includes(en)) return es;
276        }
277        if (name.includes('Night')) {
278            const dayPart = name.replace(' Night', '');
279            for (const [en, es] of Object.entries(DAY_NAMES_ES)) {
280                if (dayPart.includes(en)) return es + ' N';
281            }
282            return 'Noche';
283        }
284        return name.substring(0, 3);
285    }
286
287    function getWeatherIcon(desc) {
288        const d = desc.toLowerCase();
289        for (const [key, icon] of Object.entries(WEATHER_ICONS)) {
290            if (d.includes(key)) return icon;
291        }
292        return 'fa-cloud-sun';
293    }
294
295    // ========================================
296    // NEWS — BBC Mundo RSS
297    // ========================================
298
299    async function loadNews() {
300        const newsGrid = document.getElementById('newsGrid');
301        if (!newsGrid) return Promise.resolve();
302
303        try {
304            const articles = await fetchBBCMundoRSS();
305            if (articles && articles.length >= 4) {
306                renderNews(articles);
307                return;
308            }
309        } catch (err) {
310            console.warn('BBC Mundo RSS failed:', err);
311        }
312
313        // Show error state
314        newsGrid.innerHTML = `
315            <div class="news-loading" style="grid-column:1/-1">
316                <i class="fas fa-exclamation-triangle"></i>
317                <p>No se pudieron cargar las noticias. Intenta de nuevo.</p>
318            </div>`;
319    }
320
321    async function fetchBBCMundoRSS() {
322        // Try direct CORS proxies first
323        for (const proxy of NEWS_CONFIG.CORS_PROXIES) {
324            try {
325                const url = proxy + encodeURIComponent(NEWS_CONFIG.BBC_MUNDO_RSS);
326                const response = await fetch(url, { signal: AbortSignal.timeout(12000) });
327                if (!response.ok) continue;
328
329                const xml = await response.text();
330                if (!xml.includes('<rss') && !xml.includes('<?xml')) continue;
331                return parseBBCMundoRSS(xml);
332            } catch (e) {
333                continue;
334            }
335        }
336
337        // Fallback: allorigins /get endpoint (returns JSON wrapper with CORS)
338        try {
339            const url = NEWS_CONFIG.ALLORIGINS_GET + encodeURIComponent(NEWS_CONFIG.BBC_MUNDO_RSS);
340            const response = await fetch(url, { signal: AbortSignal.timeout(12000) });
341            if (response.ok) {
342                const json = await response.json();
343                const xml = json.contents || '';
344                if (xml.includes('<rss') || xml.includes('<?xml')) {
345                    return parseBBCMundoRSS(xml);
346                }
347            }
348        } catch (e) { /* silent */ }
349
350        return null;
351    }
352
353    function parseBBCMundoRSS(xml) {
354        const parser = new DOMParser();
355        const doc = parser.parseFromString(xml, 'text/xml');
356        const items = doc.querySelectorAll('item');
357        const articles = [];
358
359        items.forEach((item, i) => {
360            if (i >= 10) return;
361
362            const title = item.querySelector('title') ? item.querySelector('title').textContent.trim() : '';
363            const link = item.querySelector('link') ? item.querySelector('link').textContent.trim() : '';
364            const desc = item.querySelector('description') ? item.querySelector('description').textContent.trim() : '';
365            const pubDate = item.querySelector('pubDate') ? item.querySelector('pubDate').textContent.trim() : '';
366
367            if (!title || !link) return;
368
369            // Get thumbnail — BBC uses media:thumbnail namespace
370            let image = '';
371            const thumbnail = item.getElementsByTagNameNS('http://search.yahoo.com/mrss/', 'thumbnail')[0];
372            if (thumbnail) {
373                image = thumbnail.getAttribute('url') || '';
374                // Upgrade from 240px to higher res
375                if (image) {
376                    image = image.replace(/\/\d+\/cpsprodpb/, '/' + NEWS_CONFIG.BBC_IMG_SIZE + '/cpsprodpb');
377                }
378            }
379
380            const category = categorizeNewsES(title);
381            const timeAgo = pubDate ? formatPubDate(pubDate) : '';
382
383            articles.push({
384                title: title,
385                desc: cleanDescription(desc),
386                url: link,
387                image: image,
388                category: category,
389                icon: getCategoryIcon(category),
390                timeAgo: timeAgo
391            });
392        });
393
394        return articles;
395    }
396
397    function cleanDescription(desc) {
398        if (!desc) return '';
399        // Strip HTML tags from description
400        const tmp = document.createElement('div');
401        tmp.innerHTML = desc;
402        const text = tmp.textContent || tmp.innerText || '';
403        return text.length > 150 ? text.substring(0, 147) + '...' : text;
404    }
405
406    function formatPubDate(pubDateStr) {
407        try {
408            const pubDate = new Date(pubDateStr);
409            const now = new Date();
410            const diffMs = now - pubDate;
411            const diffMins = Math.floor(diffMs / 60000);
412            const diffHours = Math.floor(diffMs / 3600000);
413
414            if (diffMins < 5) return 'Hace un momento';
415            if (diffMins < 60) return `Hace ${diffMins} min`;
416            if (diffHours < 2) return 'Hace 1 hora';
417            if (diffHours < 24) return `Hace ${diffHours} horas`;
418            if (diffHours < 48) return 'Ayer';
419            return pubDate.toLocaleDateString('es-MX', { day: 'numeric', month: 'short' });
420        } catch (e) {
421            return '';
422        }
423    }
424
425    function categorizeNewsES(title) {
426        const t = title.toLowerCase();
427        if (t.includes('guerra') || t.includes('soldado') || t.includes('militar') || t.includes('ejército') || t.includes('bomba')) return 'MUNDO';
428        if (t.includes('incendio') || t.includes('rescate') || t.includes('terremoto') || t.includes('emergencia') || t.includes('erupción')) return 'EMERGENCIA';
429        if (t.includes('accidente') || t.includes('tráfico') || t.includes('carretera') || t.includes('choque')) return 'TRÁFICO';
430        if (t.includes('clima') || t.includes('tormenta') || t.includes('lluvia') || t.includes('huracán') || t.includes('frío') || t.includes('calor')) return 'CLIMA';
431        if (t.includes('policía') || t.includes('tiroteo') || t.includes('asesinato') || t.includes('crimen') || t.includes('arresto') || t.includes('narco')) return 'POLICÍA';
432        if (t.includes('vuelo') || t.includes('aeropuerto') || t.includes('avión') || t.includes('aviación')) return 'AVIACIÓN';
433        if (t.includes('salud') || t.includes('hospital') || t.includes('médico') || t.includes('enfermedad') || t.includes('virus') || t.includes('vacuna')) return 'SALUD';
434        if (t.includes('fútbol') || t.includes('deporte') || t.includes('juego') || t.includes('gol') || t.includes('olimpi')) return 'DEPORTES';
435        if (t.includes('trump') || t.includes('biden') || t.includes('congreso') || t.includes('elecciones') || t.includes('presidente') || t.includes('gobierno')) return 'POLÍTICA';
436        if (t.includes('negocio') || t.includes('economía') || t.includes('mercado') || t.includes('dólar') || t.includes('inflación')) return 'ECONOMÍA';
437        if (t.includes('tecnología') || t.includes('inteligencia artificial') || t.includes('app') || t.includes('internet')) return 'TECNOLOGÍA';
438        if (t.includes('cuba') || t.includes('venezuela') || t.includes('méxico') || t.includes('colombia') || t.includes('latino') || t.includes('migra')) return 'LATINOAMÉRICA';
439        return 'MUNDO';
440    }
441
442    function getCategoryIcon(cat) {
443        const icons = {
444            'EMERGENCIA': 'fa-fire', 'TRÁFICO': 'fa-road', 'CLIMA': 'fa-cloud-sun',
445            'POLICÍA': 'fa-shield-alt', 'AVIACIÓN': 'fa-plane', 'SALUD': 'fa-heartbeat',
446            'MUNDO': 'fa-globe-americas', 'DEPORTES': 'fa-football-ball',
447            'POLÍTICA': 'fa-landmark', 'ECONOMÍA': 'fa-chart-line',
448            'TECNOLOGÍA': 'fa-microchip', 'LATINOAMÉRICA': 'fa-globe-americas'
449        };
450        return icons[cat] || 'fa-newspaper';
451    }
452
453    function renderNews(articles) {
454        const newsGrid = document.getElementById('newsGrid');
455        if (!newsGrid) return;
456
457        if (!Array.isArray(articles) || articles.length === 0) {
458            newsGrid.innerHTML = `
459                <div class="news-loading" style="grid-column:1/-1">
460                    <i class="fas fa-exclamation-triangle"></i>
461                    <p>No hay noticias disponibles en este momento.</p>
462                </div>`;
463            return;
464        }
465
466        let html = '';
467
468        articles.forEach((article, i) => {
469            if (!article || typeof article !== 'object') return;
470
471            const isLarge = i === 0;
472            const delay = i * 0.08;
473            const safeTitle = escapeHtml(article.title || 'Sin titular');
474            const safeDesc = escapeHtml(article.desc || '');
475            const safeCategory = escapeHtml(article.category || 'MUNDO');
476            const safeTimeAgo = escapeHtml(article.timeAgo || 'Reciente');
477            const safeIcon = escapeHtml(article.icon || 'fa-newspaper');
478            const safeImage = escapeHtml(article.image || '');
479            const hasImage = safeImage.startsWith('http');
480
481            html += `
482                <div class="news-card ${isLarge ? 'large' : ''}" style="animation-delay: ${delay}s">
483                    <div class="news-image">
484                        ${hasImage ? `<img src="${safeImage}" alt="" loading="lazy" onerror="this.style.display='none';var icon=(this.parentElement&&this.parentElement.querySelector('.news-icon'))?this.parentElement.querySelector('.news-icon'):null;if(icon){icon.style.display='flex';}">` : ''}
485                        <i class="fas ${safeIcon} news-icon" ${hasImage ? 'style="display:none"' : ''}></i>
486                        <span class="news-category">${safeCategory}</span>
487                    </div>
488                    <div class="news-content">
489                        <span class="news-date"><i class="fas fa-clock"></i> ${safeTimeAgo}</span>
490                        <h4>${safeTitle}</h4>
491                        ${safeDesc ? `<p>${safeDesc}</p>` : ''}
492                        <span class="news-source-badge"><i class="fas fa-newspaper"></i> Fuente: BBC Mundo</span>
493                    </div>
494                </div>`;
495        });
496
497        newsGrid.innerHTML = html;
498    }
499
500    function escapeHtml(str) {
501        if (!str) return '';
502        const div = document.createElement('div');
503        div.textContent = str;
504        return div.innerHTML;
505    }
506
507})();

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.