Improve auto-detect place naming and add expandable 7-day forecast
This commit is contained in:
1 parent
30045c696e
commit
bd3e1e9605
4 files changed
+98
-17
No files matched your search
+62
-16
@@ -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 = `
|
||||
<div class="day-name">${formatDayShort(daily.time[i])}</div>
|
||||
<div class="day-icon">${dw.icon}</div>
|
||||
<div class="day-desc">${dw.desc}</div>
|
||||
<div class="day-temps">${tempDisplay(daily.temperature_2m_max[i])}°<span class="day-lo"> / ${tempDisplay(daily.temperature_2m_min[i])}°</span></div>
|
||||
<div class="day-meta">${daily.precipitation_probability_max[i] > 0 ? daily.precipitation_probability_max[i] + '% rain<br>' : ''}${speedDisplay(daily.wind_speed_10m_max[i])} ${speedUnit()}</div>
|
||||
`;
|
||||
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 = `
|
||||
<div class="day-name">${formatDayShort(d.time[i])}</div>
|
||||
<div class="day-icon">${dw.icon}</div>
|
||||
<div class="day-desc">${dw.desc}</div>
|
||||
<div class="day-temps">${tempDisplay(d.temperature_2m_max[i])}°<span class="day-lo"> / ${tempDisplay(d.temperature_2m_min[i])}°</span></div>
|
||||
<div class="day-meta">${d.precipitation_probability_max[i] > 0 ? d.precipitation_probability_max[i] + '% rain<br>' : ''}${speedDisplay(d.wind_speed_10m_max[i])} ${speedUnit()}</div>
|
||||
`;
|
||||
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(() => {
|
||||
|
||||
@@ -128,7 +128,10 @@
|
||||
|
||||
<!-- 7-day -->
|
||||
<div class="section-head">
|
||||
<span class="section-label">7-Day Forecast</span>
|
||||
<span id="daily-forecast-label" class="section-label">7-Day Forecast</span>
|
||||
<span class="section-note">
|
||||
<button id="daily-forecast-toggle" class="link-btn hidden" type="button">Show full forecast</button>
|
||||
</span>
|
||||
</div>
|
||||
<div id="daily-list" class="daily-list"></div>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user