// ========== WEATHER WIDGET ========== (function() { // Inject modal HTML injectModal('weather-modal', `

Weather Settings

Enter a city name, postal code, or “City, Country”
`); /* Weather Widget — powered by Open-Meteo (free, no API key) */ const LOCATION_KEY = 'weather-location'; const LEGACY_KEY = 'weather-zip'; const GEO_CACHE_KEY = 'weather-geo'; const UNIT_KEY = 'weather-unit'; // Migrate from old ZIP-only key if (!safeGet(LOCATION_KEY) && safeGet(LEGACY_KEY)) { safeSet(LOCATION_KEY, safeGet(LEGACY_KEY)); } function getUnit() { return safeGet(UNIT_KEY) || 'imperial'; } function getWeatherElements() { return { icon: document.querySelector('.weather-icon'), temp: document.querySelector('.weather-temp'), condition: document.querySelector('.weather-condition'), location: document.querySelector('.weather-location'), wind: document.querySelector('.weather-wind') }; } // WMO Weather Code mapping const wmoDescriptions = { 0: 'Clear sky', 1: 'Mainly clear', 2: 'Partly cloudy', 3: 'Overcast', 45: 'Fog', 48: 'Rime fog', 51: 'Light drizzle', 53: 'Drizzle', 55: 'Dense drizzle', 56: 'Freezing drizzle', 57: 'Dense freezing drizzle', 61: 'Light rain', 63: 'Moderate rain', 65: 'Heavy rain', 66: 'Light freezing rain', 67: 'Heavy freezing rain', 71: 'Light snow', 73: 'Moderate snow', 75: 'Heavy snow', 77: 'Snow grains', 80: 'Light showers', 81: 'Moderate showers', 82: 'Violent showers', 85: 'Light snow showers', 86: 'Heavy snow showers', 95: 'Thunderstorm', 96: 'Thunderstorm with hail', 99: 'Severe thunderstorm' }; const wmoIcons = { 0: '☀️', 1: '🌤️', 2: '⛅', 3: '☁️', 45: '🌫️', 48: '🌫️', 51: '🌦️', 53: '🌦️', 55: '🌦️', 56: '🌨️', 57: '🌨️', 61: '🌦️', 63: '🌧️', 65: '🌧️', 66: '🌨️', 67: '🌨️', 71: '🌨️', 73: '❄️', 75: '❄️', 77: '❄️', 80: '🌦️', 81: '🌧️', 82: '🌧️', 85: '🌨️', 86: '❄️', 95: '⛈️', 96: '⛈️', 99: '⛈️' }; // Wind direction from degrees const windDirs = ['N','NNE','NE','ENE','E','ESE','SE','SSE','S','SSW','SW','WSW','W','WNW','NW','NNW']; function degToDir(deg) { return windDirs[Math.round(deg / 22.5) % 16]; } // Geocode location via Open-Meteo, with localStorage cache async function geocodeLocation(query) { const cached = safeGet(GEO_CACHE_KEY); if (cached) { try { const geo = JSON.parse(cached); if (geo.query === query) return geo; } catch {} } const resp = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(query)}&count=1&language=en&format=json`); if (!resp.ok) throw new Error('Geocoding failed'); const data = await resp.json(); if (!data.results || !data.results.length) throw new Error('Location not found'); const r = data.results[0]; const geo = { query, lat: r.latitude, lon: r.longitude, city: r.name, state: r.admin1 || '', country: r.country || '', countryCode: r.country_code || '' }; safeSet(GEO_CACHE_KEY, JSON.stringify(geo)); return geo; } function formatLocation(geo) { // For US locations show "City, State", for others show "City, Country" if (geo.countryCode === 'US' && geo.state) { return `${geo.city}, ${geo.state}`; } if (geo.country) { return `${geo.city}, ${geo.country}`; } return geo.city; } async function fetchWeather(location) { try { const geo = await geocodeLocation(location); const unit = getUnit(); const tempUnit = unit === 'metric' ? 'celsius' : 'fahrenheit'; const windUnit = unit === 'metric' ? 'kmh' : 'mph'; const url = `https://api.open-meteo.com/v1/forecast?latitude=${geo.lat}&longitude=${geo.lon}¤t=temperature_2m,weather_code,wind_speed_10m,wind_direction_10m&temperature_unit=${tempUnit}&wind_speed_unit=${windUnit}`; const resp = await fetch(url); if (!resp.ok) throw new Error('Weather fetch failed'); const data = await resp.json(); const c = data.current; const code = c.weather_code; return { temp: Math.round(c.temperature_2m), condition: wmoDescriptions[code] || 'Unknown', icon: wmoIcons[code] || '🌤️', locationStr: formatLocation(geo), windSpeed: Math.round(c.wind_speed_10m), windDir: degToDir(c.wind_direction_10m), unit }; } catch (error) { console.warn('Weather fetch failed:', error); return null; } } async function updateWeather() { const weatherWidget = getWeatherElements(); if (!weatherWidget.icon || !weatherWidget.temp || !weatherWidget.condition || !weatherWidget.location || !weatherWidget.wind) { console.warn('Weather widget elements not found'); return; } const location = safeGet(LOCATION_KEY); if (!location) { weatherWidget.location.textContent = 'Set Location'; weatherWidget.temp.textContent = '--\u00B0'; weatherWidget.condition.textContent = 'Click \u2699\uFE0F to configure'; weatherWidget.wind.textContent = '--'; weatherWidget.icon.innerHTML = '\uD83C\uDF24\uFE0F'; return; } try { const weather = await fetchWeather(location); if (weather) { const tempSuffix = weather.unit === 'metric' ? '\u00B0C' : '\u00B0F'; const windLabel = weather.unit === 'metric' ? 'km/h' : 'mph'; weatherWidget.location.textContent = weather.locationStr; weatherWidget.temp.textContent = `${weather.temp}${tempSuffix}`; weatherWidget.condition.textContent = weather.condition; weatherWidget.wind.textContent = `Wind: ${weather.windSpeed} ${windLabel} ${weather.windDir}`; weatherWidget.icon.innerHTML = `${weather.icon}`; } } catch (error) { console.error('Weather update error:', error); weatherWidget.location.textContent = 'Weather Error'; weatherWidget.temp.textContent = 'Error'; weatherWidget.condition.textContent = 'Failed to load'; weatherWidget.wind.textContent = '--'; } } // Weather settings modal const modal = document.getElementById('weather-modal'); const locationInput = document.getElementById('weather-location-input'); document.getElementById('weather-settings')?.addEventListener('click', () => { locationInput.value = safeGet(LOCATION_KEY) || ''; // Set unit radio const unit = getUnit(); const radio = modal.querySelector(`input[name="weather-unit-radio"][value="${unit}"]`); if (radio) radio.checked = true; modal.classList.add('show'); locationInput.focus(); }); document.getElementById('weather-cancel')?.addEventListener('click', () => { modal.classList.remove('show'); }); document.getElementById('weather-save')?.addEventListener('click', () => { const loc = locationInput.value.trim(); if (loc) { // Clear geo cache when location changes so it re-geocodes const oldLoc = safeGet(LOCATION_KEY); if (oldLoc !== loc) safeSet(GEO_CACHE_KEY, ''); safeSet(LOCATION_KEY, loc); // Save unit preference const unitRadio = modal.querySelector('input[name="weather-unit-radio"]:checked'); const newUnit = unitRadio ? unitRadio.value : 'imperial'; const oldUnit = getUnit(); safeSet(UNIT_KEY, newUnit); // Clear geo cache if unit changed (need fresh weather data) if (oldUnit !== newUnit) safeSet(GEO_CACHE_KEY, ''); modal.classList.remove('show'); updateWeather(); } else { showNotification('Please enter a location (e.g., Hamburg, London, 90210)', 'warning'); } }); wireModal(modal); // Close modal on Escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modal.classList.contains('show')) { modal.classList.remove('show'); } }); // Initialize weather updateWeather(); // Update weather every 10 minutes setInterval(updateWeather, DC.POLL.WEATHER); })();