Add lightning strikes and wind overlay toggles to Maps tab

This commit is contained in:
Nate committed 2026-03-16 11:56:03 -04:00
1 parent 02f49e55f8
commit e4c631f9a8
4 files changed
+1088 -92

No files matched your search

+727 -84
View File
File diff suppressed because it is too large. Load diff
+99 -1
View File
@@ -25,6 +25,9 @@
<nav class="tabs"> <nav class="tabs">
<button class="tab active" data-tab="conditions">Conditions</button> <button class="tab active" data-tab="conditions">Conditions</button>
<button class="tab" data-tab="radar-full">Radar</button> <button class="tab" data-tab="radar-full">Radar</button>
<button class="tab" data-tab="maps">Maps</button>
<button class="tab" data-tab="alerts">Alerts</button>
<button class="tab" data-tab="settings">Settings</button>
</nav> </nav>
<span class="tagline">Open-Meteo · RainViewer</span> <span class="tagline">Open-Meteo · RainViewer</span>
</header> </header>
@@ -35,6 +38,7 @@
<input id="search-input" type="text" placeholder="City, region, or country…" autocomplete="off" spellcheck="false"> <input id="search-input" type="text" placeholder="City, region, or country…" autocomplete="off" spellcheck="false">
<button id="search-btn">Search</button> <button id="search-btn">Search</button>
<button id="locate-btn" title="Use my location">⌖</button> <button id="locate-btn" title="Use my location">⌖</button>
<button id="units-btn" title="Toggle units">US</button>
</div> </div>
<ul id="suggestions" class="suggestions hidden"></ul> <ul id="suggestions" class="suggestions hidden"></ul>
<p id="error-msg" class="error hidden"></p> <p id="error-msg" class="error hidden"></p>
@@ -54,7 +58,10 @@
<div id="loc-name" class="loc-name"></div> <div id="loc-name" class="loc-name"></div>
<div id="loc-sub" class="loc-sub"></div> <div id="loc-sub" class="loc-sub"></div>
</div> </div>
<div id="loc-time" class="loc-time"></div> <div class="loc-actions">
<div id="loc-time" class="loc-time"></div>
<button id="share-btn" class="link-btn" type="button">Share location</button>
</div>
</div> </div>
<!-- Current hero --> <!-- Current hero -->
@@ -81,6 +88,18 @@
<div class="stat"><div class="stat-label">Cloud Cover</div><div class="stat-val"><span id="cur-cloud">--</span><span class="unit">%</span></div></div> <div class="stat"><div class="stat-label">Cloud Cover</div><div class="stat-val"><span id="cur-cloud">--</span><span class="unit">%</span></div></div>
</div> </div>
<div class="astro-grid">
<div class="astro-card"><div class="stat-label">Sunrise</div><div id="sunrise-val" class="astro-val">--</div></div>
<div class="astro-card"><div class="stat-label">Sunset</div><div id="sunset-val" class="astro-val">--</div></div>
<div class="astro-card"><div class="stat-label">Moon Phase</div><div id="moon-phase-val" class="astro-val">--</div></div>
</div>
<div class="aq-grid">
<div class="aq-card"><div class="stat-label">US AQI</div><div id="aqi-us" class="aq-val">--</div></div>
<div class="aq-card"><div class="stat-label">PM2.5</div><div id="pm25" class="aq-val">--</div></div>
<div class="aq-card"><div class="stat-label">Pollen</div><div id="pollen" class="aq-val">--</div></div>
</div>
<!-- Embedded radar panel --> <!-- Embedded radar panel -->
<div class="section-head"> <div class="section-head">
<span class="section-label">Radar</span> <span class="section-label">Radar</span>
@@ -92,6 +111,7 @@
<div id="radar-embed" class="radar-map"></div> <div id="radar-embed" class="radar-map"></div>
<div class="radar-controls"> <div class="radar-controls">
<button id="embed-play" class="radar-btn play-btn" title="Play/Pause">▶</button> <button id="embed-play" class="radar-btn play-btn" title="Play/Pause">▶</button>
<button id="embed-layer" class="radar-btn layer-btn" title="Toggle radar/satellite">R</button>
<div class="radar-timeline" id="embed-timeline"></div> <div class="radar-timeline" id="embed-timeline"></div>
<span id="embed-timestamp" class="radar-ts">--</span> <span id="embed-timestamp" class="radar-ts">--</span>
</div> </div>
@@ -126,6 +146,7 @@
<div id="radar-full" class="radar-map radar-map-full"></div> <div id="radar-full" class="radar-map radar-map-full"></div>
<div class="radar-controls radar-controls-full"> <div class="radar-controls radar-controls-full">
<button id="full-play" class="radar-btn play-btn" title="Play/Pause">▶</button> <button id="full-play" class="radar-btn play-btn" title="Play/Pause">▶</button>
<button id="full-layer" class="radar-btn layer-btn" title="Toggle radar/satellite">R</button>
<div class="radar-timeline" id="full-timeline"></div> <div class="radar-timeline" id="full-timeline"></div>
<span id="full-timestamp" class="radar-ts">--</span> <span id="full-timestamp" class="radar-ts">--</span>
<div class="radar-legend"> <div class="radar-legend">
@@ -138,6 +159,83 @@
<p class="radar-attribution">Radar tiles © <a href="https://www.rainviewer.com" target="_blank">RainViewer</a> · Map © <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a></p> <p class="radar-attribution">Radar tiles © <a href="https://www.rainviewer.com" target="_blank">RainViewer</a> · Map © <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a></p>
</div> </div>
<!-- ── TAB: MAPS ── -->
<div id="tab-maps" class="tab-panel hidden">
<div class="maps-wrap">
<div id="map-overview" class="radar-map map-overview"></div>
<div class="maps-meta">
<div class="maps-card">
<div class="stat-label">Data Source Location</div>
<div id="maps-location" class="maps-value">--</div>
</div>
<div class="maps-card">
<div class="stat-label">Current Wind</div>
<div id="maps-wind" class="maps-value">--</div>
</div>
</div>
<p class="panel-note">Toggle lightning strikes and wind overlay in Settings.</p>
</div>
</div>
<!-- ── TAB: ALERTS ── -->
<div id="tab-alerts" class="tab-panel hidden">
<div id="alerts-wrap" class="alerts-wrap hidden">
<div class="section-head">
<span class="section-label">Weather Alerts</span>
<button id="notify-btn" class="link-btn" type="button">Enable notifications</button>
</div>
<div id="alerts-list" class="alerts-list"></div>
</div>
<p id="alerts-empty" class="panel-note">No active alerts for this location.</p>
</div>
<!-- ── TAB: SETTINGS ── -->
<div id="tab-settings" class="tab-panel hidden">
<div class="settings-wrap">
<div class="saved-wrap">
<div class="saved-head">
<span>Saved locations</span>
<button id="save-current-btn" class="link-btn" type="button">Save current</button>
</div>
<div id="saved-locations" class="saved-locations"></div>
</div>
<div class="settings-row">
<span class="settings-label">Auto-detect location on startup</span>
<label class="auto-detect-toggle" for="auto-detect-toggle">
<input id="auto-detect-toggle" type="checkbox" checked>
<span>Enabled</span>
</label>
</div>
<div class="settings-row">
<span class="settings-label">Data source location</span>
<span id="settings-location" class="settings-value">--</span>
</div>
<div class="settings-row">
<span class="settings-label">Units</span>
<button id="units-btn-settings" class="settings-btn" type="button">US</button>
</div>
<div class="settings-row">
<span class="settings-label">Lightning strikes on map</span>
<label class="toggle-label" for="lightning-toggle">
<input id="lightning-toggle" type="checkbox">
<span id="lightning-status">Off</span>
</label>
</div>
<div class="settings-row">
<span class="settings-label">Wind overlay on map</span>
<label class="toggle-label" for="wind-toggle">
<input id="wind-toggle" type="checkbox">
<span id="wind-status">Off</span>
</label>
</div>
</div>
</div>
</div><!-- .app --> </div><!-- .app -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
+217 -1
View File
@@ -148,6 +148,88 @@ a:hover { text-decoration: underline; }
padding: 0.6rem 0.9rem !important; padding: 0.6rem 0.9rem !important;
} }
#units-btn {
background: var(--paper-dark) !important;
color: var(--ink) !important;
border-left: none !important;
min-width: 76px;
}
.saved-wrap { margin-top: 0.6rem; }
.saved-head {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.62rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink-light);
margin-bottom: 0.35rem;
}
.saved-locations {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
font-size: 0.72rem;
color: var(--ink-mid);
}
.auto-detect-toggle {
display: inline-flex;
align-items: center;
gap: 0.45rem;
margin-bottom: 0.45rem;
font-size: 0.68rem;
color: var(--ink-mid);
cursor: pointer;
user-select: none;
}
.auto-detect-toggle input {
accent-color: var(--accent);
}
.toggle-label {
display: inline-flex;
align-items: center;
gap: 0.45rem;
margin-bottom: 0.45rem;
font-size: 0.68rem;
color: var(--ink-mid);
cursor: pointer;
user-select: none;
}
.toggle-label input {
accent-color: var(--accent);
}
.toggle-label span {
min-width: 24px;
}
.saved-item {
display: inline-flex;
border: 1px solid var(--border);
background: var(--paper-dark);
}
.saved-chip,
.saved-remove {
border: none;
background: transparent;
color: var(--ink);
padding: 0.35rem 0.45rem;
font-family: var(--mono);
cursor: pointer;
}
.saved-remove {
border-left: 1px solid var(--border);
color: var(--ink-light);
}
/* Suggestions */ /* Suggestions */
.suggestions { .suggestions {
position: absolute; position: absolute;
@@ -212,6 +294,7 @@ a:hover { text-decoration: underline; }
.loc-name { font-family: var(--serif); font-size: 1.6rem; font-weight: 400; line-height: 1.1; } .loc-name { font-family: var(--serif); font-size: 1.6rem; font-weight: 400; line-height: 1.1; }
.loc-sub { font-size: 0.68rem; color: var(--ink-light); letter-spacing: 0.06em; margin-top: 0.2rem; text-transform: uppercase; } .loc-sub { font-size: 0.68rem; color: var(--ink-light); letter-spacing: 0.06em; margin-top: 0.2rem; text-transform: uppercase; }
.loc-time { font-size: 0.68rem; color: var(--ink-light); text-align: right; letter-spacing: 0.04em; line-height: 1.5; } .loc-time { font-size: 0.68rem; color: var(--ink-light); text-align: right; letter-spacing: 0.04em; line-height: 1.5; }
.loc-actions { display: flex; flex-direction: column; align-items: flex-end; gap: 0.35rem; }
/* ── Current hero ── */ /* ── Current hero ── */
.current-grid { .current-grid {
@@ -253,6 +336,129 @@ a:hover { text-decoration: underline; }
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.astro-grid,
.aq-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1px;
background: var(--border);
border: 1px solid var(--border);
margin-bottom: 1rem;
}
.astro-card,
.aq-card {
background: var(--paper-dark);
padding: 0.85rem 0.75rem;
}
.astro-val,
.aq-val {
font-size: 0.95rem;
font-weight: 500;
}
.alerts-wrap {
border: 1px solid var(--border);
padding: 0.75rem;
margin-bottom: 1.2rem;
background: var(--paper-dark);
}
.alerts-list {
display: grid;
gap: 0.5rem;
}
.alert-item {
display: grid;
gap: 0.2rem;
border: 1px solid var(--border);
padding: 0.55rem;
background: var(--paper);
font-size: 0.72rem;
}
.panel-note {
font-size: 0.72rem;
color: var(--ink-light);
border: 1px solid var(--border);
padding: 0.75rem;
background: var(--paper-dark);
}
.settings-wrap {
border: 1px solid var(--border);
background: var(--paper-dark);
padding: 0.9rem;
display: grid;
gap: 1rem;
}
.settings-row {
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid var(--border);
background: var(--paper);
padding: 0.65rem 0.75rem;
}
.settings-label {
font-size: 0.72rem;
color: var(--ink-mid);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.settings-value {
font-size: 0.8rem;
color: var(--ink);
text-align: right;
}
.settings-btn {
border: 1px solid var(--border);
background: var(--paper-darker);
color: var(--ink);
padding: 0.45rem 0.7rem;
font-family: var(--mono);
font-size: 0.72rem;
cursor: pointer;
}
.settings-btn:hover { background: var(--paper-dark); }
.maps-wrap {
border: 1px solid var(--border);
background: var(--paper-dark);
padding: 0.75rem;
}
.map-overview {
height: 360px;
border: 1px solid var(--border);
}
.maps-meta {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1px;
margin-top: 0.75rem;
background: var(--border);
border: 1px solid var(--border);
}
.maps-card {
background: var(--paper);
padding: 0.7rem;
}
.maps-value {
font-size: 0.85rem;
color: var(--ink);
}
.stat { background: var(--paper-dark); padding: 0.9rem 0.75rem; text-align: center; } .stat { background: var(--paper-dark); padding: 0.9rem 0.75rem; text-align: center; }
.stat-label { font-size: 0.58rem; letter-spacing: 0.1em; color: var(--ink-light); text-transform: uppercase; margin-bottom: 0.3rem; } .stat-label { font-size: 0.58rem; letter-spacing: 0.1em; color: var(--ink-light); text-transform: uppercase; margin-bottom: 0.3rem; }
.stat-val { font-size: 1rem; font-weight: 500; } .stat-val { font-size: 1rem; font-weight: 500; }
@@ -330,6 +536,7 @@ a:hover { text-decoration: underline; }
} }
.radar-btn:hover { opacity: 0.72; } .radar-btn:hover { opacity: 0.72; }
.layer-btn { background: var(--accent); }
.radar-timeline { .radar-timeline {
flex: 1; flex: 1;
@@ -500,12 +707,21 @@ a:hover { text-decoration: underline; }
.masthead { grid-template-columns: 1fr 1fr; grid-template-rows: auto auto; } .masthead { grid-template-columns: 1fr 1fr; grid-template-rows: auto auto; }
.masthead h1 { font-size: 1.6rem; } .masthead h1 { font-size: 1.6rem; }
.tagline { display: none; } .tagline { display: none; }
.tabs { grid-column: 1 / -1; } .tabs { grid-column: 1 / -1; overflow-x: auto; }
.tab { min-width: 88px; }
.stats-bar { grid-template-columns: repeat(3, 1fr); } .stats-bar { grid-template-columns: repeat(3, 1fr); }
.astro-grid,
.aq-grid { grid-template-columns: 1fr; }
.current-grid { grid-template-columns: 1fr; } .current-grid { grid-template-columns: 1fr; }
.temp-big { font-size: 4rem; } .temp-big { font-size: 4rem; }
.day-row { grid-template-columns: 80px 36px 1fr 70px; } .day-row { grid-template-columns: 80px 36px 1fr 70px; }
.day-meta { display: none; } .day-meta { display: none; }
.radar-map { height: 240px; } .radar-map { height: 240px; }
.legend-bar { width: 50px; } .legend-bar { width: 50px; }
.loc-actions { align-items: flex-start; margin-top: 0.5rem; }
.loc-header { flex-direction: column; align-items: flex-start; }
.settings-row { flex-direction: column; align-items: flex-start; gap: 0.5rem; }
.settings-value { text-align: left; }
.map-overview { height: 280px; }
.maps-meta { grid-template-columns: 1fr; }
} }
+45 -6
View File
@@ -22,9 +22,10 @@ app.get('/api/geocode', async (req, res) => {
// Weather proxy // Weather proxy
app.get('/api/weather', async (req, res) => { app.get('/api/weather', async (req, res) => {
const { lat, lon, tz } = req.query; const { lat, lon, tz, units } = req.query;
if (!lat || !lon) return res.status(400).json({ error: 'Missing lat/lon' }); if (!lat || !lon) return res.status(400).json({ error: 'Missing lat/lon' });
const timezone = tz || 'auto'; const timezone = tz || 'auto';
const useMetric = units === 'metric';
try { try {
const params = new URLSearchParams({ const params = new URLSearchParams({
latitude: lat, longitude: lon, timezone, latitude: lat, longitude: lon, timezone,
@@ -35,17 +36,17 @@ app.get('/api/weather', async (req, res) => {
].join(','), ].join(','),
hourly: [ hourly: [
'temperature_2m','precipitation_probability','weather_code', 'temperature_2m','precipitation_probability','weather_code',
'wind_speed_10m','apparent_temperature' 'wind_speed_10m','apparent_temperature','rain','snowfall'
].join(','), ].join(','),
daily: [ daily: [
'weather_code','temperature_2m_max','temperature_2m_min', 'weather_code','temperature_2m_max','temperature_2m_min',
'precipitation_sum','precipitation_probability_max', 'precipitation_sum','precipitation_probability_max',
'wind_speed_10m_max','sunrise','sunset','uv_index_max' 'wind_speed_10m_max','sunrise','sunset','uv_index_max'
].join(','), ].join(','),
forecast_days: 7, forecast_days: 16,
wind_speed_unit: 'mph', wind_speed_unit: useMetric ? 'kmh' : 'mph',
temperature_unit: 'fahrenheit', temperature_unit: useMetric ? 'celsius' : 'fahrenheit',
precipitation_unit: 'inch' precipitation_unit: useMetric ? 'mm' : 'inch'
}); });
const r = await fetch(`https://api.open-meteo.com/v1/forecast?${params}`); const r = await fetch(`https://api.open-meteo.com/v1/forecast?${params}`);
const data = await r.json(); const data = await r.json();
@@ -55,6 +56,44 @@ app.get('/api/weather', async (req, res) => {
} }
}); });
// Open-Meteo Air Quality proxy
app.get('/api/air-quality', async (req, res) => {
const { lat, lon, tz } = req.query;
if (!lat || !lon) return res.status(400).json({ error: 'Missing lat/lon' });
try {
const params = new URLSearchParams({
latitude: lat,
longitude: lon,
timezone: tz || 'auto',
current: ['us_aqi', 'pm2_5', 'alder_pollen', 'birch_pollen', 'grass_pollen', 'mugwort_pollen', 'olive_pollen', 'ragweed_pollen'].join(','),
forecast_days: 1,
});
const r = await fetch(`https://air-quality-api.open-meteo.com/v1/air-quality?${params}`);
const data = await r.json();
res.json(data);
} catch (e) {
res.status(500).json({ error: 'Air quality fetch failed', detail: e.message });
}
});
// NWS alerts proxy (US only)
app.get('/api/alerts', async (req, res) => {
const { lat, lon } = req.query;
if (!lat || !lon) return res.status(400).json({ error: 'Missing lat/lon' });
try {
const r = await fetch(`https://api.weather.gov/alerts/active?point=${lat},${lon}`, {
headers: {
'User-Agent': 'weather-app-v2 (nate@necloud.us)',
Accept: 'application/geo+json',
},
});
const data = await r.json();
res.json(data);
} catch (e) {
res.status(500).json({ error: 'Alerts fetch failed', detail: e.message });
}
});
// RainViewer timestamps proxy // RainViewer timestamps proxy
app.get('/api/radar/times', async (req, res) => { app.get('/api/radar/times', async (req, res) => {
try { try {