diff --git a/app/public/app.js b/app/public/app.js index 5fbeec1..a5059f1 100644 --- a/app/public/app.js +++ b/app/public/app.js @@ -48,6 +48,7 @@ const state = { lastRefreshAt: 0, lightningMarkers: [], windArrows: [], + dailyExpanded: false, }; // ── DOM ────────────────────────────────────────────────────────────────────── @@ -73,6 +74,8 @@ const lightningToggle = document.getElementById('lightning-toggle'); const windToggle = document.getElementById('wind-toggle'); const lightningStatus = document.getElementById('lightning-status'); const windStatus = document.getElementById('wind-status'); +const dailyForecastLabel = document.getElementById('daily-forecast-label'); +const dailyForecastToggle = document.getElementById('daily-forecast-toggle'); // ── Helpers ────────────────────────────────────────────────────────────────── const show = el => el.classList.remove('hidden'); @@ -310,7 +313,7 @@ async function fetchAlerts(lat, lon) { function detectedCityFallback(lat, lon) { return { - name: `Detected location (${lat.toFixed(2)}, ${lon.toFixed(2)})`, + name: `Auto-detected (${lat.toFixed(2)}, ${lon.toFixed(2)})`, latitude: lat, longitude: lon, country: '', @@ -319,6 +322,17 @@ function detectedCityFallback(lat, lon) { }; } +function resolveDetectedName(place, fallback) { + const raw = (place?.name || '').trim(); + const genericName = !raw || /current\s*location|detected\s*location/i.test(raw); + if (!genericName) return raw; + + const displayRoot = (place?.display_name || '').split(',')[0]?.trim(); + if (displayRoot) return displayRoot; + + return fallback.name; +} + async function buildDetectedCity(lat, lon) { const fallback = detectedCityFallback(lat, lon); @@ -327,6 +341,7 @@ async function buildDetectedCity(lat, lon) { return { ...fallback, ...place, + name: resolveDetectedName(place, fallback), latitude: lat, longitude: lon, timezone: place.timezone || 'auto', @@ -639,6 +654,43 @@ function renderWindArrows(city) { const mapsOverview = createOverviewMap(document.getElementById('map-overview')); // ── Rendering ──────────────────────────────────────────────────────────────── +function renderDailyForecast(daily) { + const list = document.getElementById('daily-list'); + const totalDays = daily?.time?.length || 0; + const defaultDays = Math.min(7, totalDays); + const visibleDays = state.dailyExpanded ? totalDays : defaultDays; + + list.innerHTML = ''; + for (let i = 0; i < visibleDays; i++) { + const dw = wmo(daily.weather_code[i]); + const row = document.createElement('div'); + row.className = 'day-row' + (i === 0 ? ' today' : ''); + row.innerHTML = ` +
${formatDayShort(daily.time[i])}
+
${dw.icon}
+
${dw.desc}
+
${tempDisplay(daily.temperature_2m_max[i])}° / ${tempDisplay(daily.temperature_2m_min[i])}°
+
${daily.precipitation_probability_max[i] > 0 ? daily.precipitation_probability_max[i] + '% rain
' : ''}${speedDisplay(daily.wind_speed_10m_max[i])} ${speedUnit()}
+ `; + list.appendChild(row); + } + + if (dailyForecastLabel) { + dailyForecastLabel.textContent = `${visibleDays}-Day Forecast`; + } + + if (dailyForecastToggle) { + if (totalDays > defaultDays) { + show(dailyForecastToggle); + dailyForecastToggle.textContent = state.dailyExpanded + ? 'Show 7-day view' + : `Show full ${totalDays}-day forecast`; + } else { + hide(dailyForecastToggle); + } + } +} + function renderWeather(data, city) { const c = data.current; const h = data.hourly; @@ -695,21 +747,7 @@ function renderWeather(data, city) { track.appendChild(cell); } - const list = document.getElementById('daily-list'); - list.innerHTML = ''; - for (let i = 0; i < d.time.length; i++) { - const dw = wmo(d.weather_code[i]); - const row = document.createElement('div'); - row.className = 'day-row' + (i === 0 ? ' today' : ''); - row.innerHTML = ` -
${formatDayShort(d.time[i])}
-
${dw.icon}
-
${dw.desc}
-
${tempDisplay(d.temperature_2m_max[i])}° / ${tempDisplay(d.temperature_2m_min[i])}°
-
${d.precipitation_probability_max[i] > 0 ? d.precipitation_probability_max[i] + '% rain
' : ''}${speedDisplay(d.wind_speed_10m_max[i])} ${speedUnit()}
- `; - list.appendChild(row); - } + renderDailyForecast(d); document.getElementById('updated-at').textContent = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); @@ -996,6 +1034,14 @@ windToggle.addEventListener('change', () => { } }); +if (dailyForecastToggle) { + dailyForecastToggle.addEventListener('click', () => { + if (!state.currentWeather?.daily) return; + state.dailyExpanded = !state.dailyExpanded; + renderDailyForecast(state.currentWeather.daily); + }); +} + function startAutoRefresh() { clearInterval(state.autoRefreshTimer); state.autoRefreshTimer = setInterval(() => { diff --git a/app/public/index.html b/app/public/index.html index d45ef07..c1bd440 100644 --- a/app/public/index.html +++ b/app/public/index.html @@ -128,7 +128,10 @@
- 7-Day Forecast + 7-Day Forecast + + +
diff --git a/app/public/style.css b/app/public/style.css index 546c81a..bf1d62d 100644 --- a/app/public/style.css +++ b/app/public/style.css @@ -652,6 +652,12 @@ a:hover { text-decoration: underline; } .day-lo { color: var(--ink-light); } .day-meta { text-align: right; font-size: 0.62rem; color: var(--ink-light); line-height: 1.6; } +#daily-forecast-toggle { + font-size: 0.66rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + /* ── Footer ── */ .footer-note { font-size: 0.62rem; diff --git a/app/server.js b/app/server.js index cb2bb0d..c938732 100644 --- a/app/server.js +++ b/app/server.js @@ -26,6 +26,32 @@ app.get('/api/reverse-geocode', async (req, res) => { if (!lat || !lon) return res.status(400).json({ error: 'Missing lat/lon' }); try { + // Prefer Open-Meteo reverse geocoding for stable city/region labels. + const omParams = new URLSearchParams({ + latitude: String(lat), + longitude: String(lon), + count: '1', + language: 'en', + format: 'json', + }); + + const omResp = await fetch(`https://geocoding-api.open-meteo.com/v1/reverse?${omParams}`); + if (omResp.ok) { + const omData = await omResp.json(); + const top = omData?.results?.[0]; + if (top) { + return res.json({ + name: top.name || top.admin2 || top.admin1 || top.country || 'Detected location', + admin1: top.admin1 || top.admin2 || '', + country: top.country || '', + latitude: Number(lat), + longitude: Number(lon), + timezone: top.timezone || 'auto', + display_name: [top.name, top.admin1, top.country].filter(Boolean).join(', '), + }); + } + } + const params = new URLSearchParams({ format: 'jsonv2', lat: String(lat),