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,
|
lastRefreshAt: 0,
|
||||||
lightningMarkers: [],
|
lightningMarkers: [],
|
||||||
windArrows: [],
|
windArrows: [],
|
||||||
|
dailyExpanded: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── DOM ──────────────────────────────────────────────────────────────────────
|
// ── DOM ──────────────────────────────────────────────────────────────────────
|
||||||
@@ -73,6 +74,8 @@ const lightningToggle = document.getElementById('lightning-toggle');
|
|||||||
const windToggle = document.getElementById('wind-toggle');
|
const windToggle = document.getElementById('wind-toggle');
|
||||||
const lightningStatus = document.getElementById('lightning-status');
|
const lightningStatus = document.getElementById('lightning-status');
|
||||||
const windStatus = document.getElementById('wind-status');
|
const windStatus = document.getElementById('wind-status');
|
||||||
|
const dailyForecastLabel = document.getElementById('daily-forecast-label');
|
||||||
|
const dailyForecastToggle = document.getElementById('daily-forecast-toggle');
|
||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
const show = el => el.classList.remove('hidden');
|
const show = el => el.classList.remove('hidden');
|
||||||
@@ -310,7 +313,7 @@ async function fetchAlerts(lat, lon) {
|
|||||||
|
|
||||||
function detectedCityFallback(lat, lon) {
|
function detectedCityFallback(lat, lon) {
|
||||||
return {
|
return {
|
||||||
name: `Detected location (${lat.toFixed(2)}, ${lon.toFixed(2)})`,
|
name: `Auto-detected (${lat.toFixed(2)}, ${lon.toFixed(2)})`,
|
||||||
latitude: lat,
|
latitude: lat,
|
||||||
longitude: lon,
|
longitude: lon,
|
||||||
country: '',
|
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) {
|
async function buildDetectedCity(lat, lon) {
|
||||||
const fallback = detectedCityFallback(lat, lon);
|
const fallback = detectedCityFallback(lat, lon);
|
||||||
|
|
||||||
@@ -327,6 +341,7 @@ async function buildDetectedCity(lat, lon) {
|
|||||||
return {
|
return {
|
||||||
...fallback,
|
...fallback,
|
||||||
...place,
|
...place,
|
||||||
|
name: resolveDetectedName(place, fallback),
|
||||||
latitude: lat,
|
latitude: lat,
|
||||||
longitude: lon,
|
longitude: lon,
|
||||||
timezone: place.timezone || 'auto',
|
timezone: place.timezone || 'auto',
|
||||||
@@ -639,6 +654,43 @@ function renderWindArrows(city) {
|
|||||||
const mapsOverview = createOverviewMap(document.getElementById('map-overview'));
|
const mapsOverview = createOverviewMap(document.getElementById('map-overview'));
|
||||||
|
|
||||||
// ── Rendering ────────────────────────────────────────────────────────────────
|
// ── 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) {
|
function renderWeather(data, city) {
|
||||||
const c = data.current;
|
const c = data.current;
|
||||||
const h = data.hourly;
|
const h = data.hourly;
|
||||||
@@ -695,21 +747,7 @@ function renderWeather(data, city) {
|
|||||||
track.appendChild(cell);
|
track.appendChild(cell);
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = document.getElementById('daily-list');
|
renderDailyForecast(d);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('updated-at').textContent =
|
document.getElementById('updated-at').textContent =
|
||||||
new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
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() {
|
function startAutoRefresh() {
|
||||||
clearInterval(state.autoRefreshTimer);
|
clearInterval(state.autoRefreshTimer);
|
||||||
state.autoRefreshTimer = setInterval(() => {
|
state.autoRefreshTimer = setInterval(() => {
|
||||||
|
|||||||
@@ -128,7 +128,10 @@
|
|||||||
|
|
||||||
<!-- 7-day -->
|
<!-- 7-day -->
|
||||||
<div class="section-head">
|
<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>
|
||||||
<div id="daily-list" class="daily-list"></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-lo { color: var(--ink-light); }
|
||||||
.day-meta { text-align: right; font-size: 0.62rem; color: var(--ink-light); line-height: 1.6; }
|
.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 ── */
|
||||||
.footer-note {
|
.footer-note {
|
||||||
font-size: 0.62rem;
|
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' });
|
if (!lat || !lon) return res.status(400).json({ error: 'Missing lat/lon' });
|
||||||
|
|
||||||
try {
|
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({
|
const params = new URLSearchParams({
|
||||||
format: 'jsonv2',
|
format: 'jsonv2',
|
||||||
lat: String(lat),
|
lat: String(lat),
|
||||||
|
|||||||
Reference in new issue
Block a user