Add lightning strikes and wind overlay toggles to Maps tab
This commit is contained in:
1 parent
02f49e55f8
commit
e4c631f9a8
4 files changed
+1088
-92
No files matched your search
+727
-84
File diff suppressed because it is too large.
Load diff
+99
-1
@@ -25,6 +25,9 @@
|
||||
<nav class="tabs">
|
||||
<button class="tab active" data-tab="conditions">Conditions</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>
|
||||
<span class="tagline">Open-Meteo · RainViewer</span>
|
||||
</header>
|
||||
@@ -35,6 +38,7 @@
|
||||
<input id="search-input" type="text" placeholder="City, region, or country…" autocomplete="off" spellcheck="false">
|
||||
<button id="search-btn">Search</button>
|
||||
<button id="locate-btn" title="Use my location">⌖</button>
|
||||
<button id="units-btn" title="Toggle units">US</button>
|
||||
</div>
|
||||
<ul id="suggestions" class="suggestions hidden"></ul>
|
||||
<p id="error-msg" class="error hidden"></p>
|
||||
@@ -54,7 +58,10 @@
|
||||
<div id="loc-name" class="loc-name"></div>
|
||||
<div id="loc-sub" class="loc-sub"></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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<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 -->
|
||||
<div class="section-head">
|
||||
<span class="section-label">Radar</span>
|
||||
@@ -92,6 +111,7 @@
|
||||
<div id="radar-embed" class="radar-map"></div>
|
||||
<div class="radar-controls">
|
||||
<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>
|
||||
<span id="embed-timestamp" class="radar-ts">--</span>
|
||||
</div>
|
||||
@@ -126,6 +146,7 @@
|
||||
<div id="radar-full" class="radar-map radar-map-full"></div>
|
||||
<div class="radar-controls radar-controls-full">
|
||||
<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>
|
||||
<span id="full-timestamp" class="radar-ts">--</span>
|
||||
<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>
|
||||
</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 -->
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
|
||||
+217
-1
@@ -148,6 +148,88 @@ a:hover { text-decoration: underline; }
|
||||
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 {
|
||||
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-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-actions { display: flex; flex-direction: column; align-items: flex-end; gap: 0.35rem; }
|
||||
|
||||
/* ── Current hero ── */
|
||||
.current-grid {
|
||||
@@ -253,6 +336,129 @@ a:hover { text-decoration: underline; }
|
||||
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-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; }
|
||||
@@ -330,6 +536,7 @@ a:hover { text-decoration: underline; }
|
||||
}
|
||||
|
||||
.radar-btn:hover { opacity: 0.72; }
|
||||
.layer-btn { background: var(--accent); }
|
||||
|
||||
.radar-timeline {
|
||||
flex: 1;
|
||||
@@ -500,12 +707,21 @@ a:hover { text-decoration: underline; }
|
||||
.masthead { grid-template-columns: 1fr 1fr; grid-template-rows: auto auto; }
|
||||
.masthead h1 { font-size: 1.6rem; }
|
||||
.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); }
|
||||
.astro-grid,
|
||||
.aq-grid { grid-template-columns: 1fr; }
|
||||
.current-grid { grid-template-columns: 1fr; }
|
||||
.temp-big { font-size: 4rem; }
|
||||
.day-row { grid-template-columns: 80px 36px 1fr 70px; }
|
||||
.day-meta { display: none; }
|
||||
.radar-map { height: 240px; }
|
||||
.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
@@ -22,9 +22,10 @@ app.get('/api/geocode', async (req, res) => {
|
||||
|
||||
// Weather proxy
|
||||
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' });
|
||||
const timezone = tz || 'auto';
|
||||
const useMetric = units === 'metric';
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
latitude: lat, longitude: lon, timezone,
|
||||
@@ -35,17 +36,17 @@ app.get('/api/weather', async (req, res) => {
|
||||
].join(','),
|
||||
hourly: [
|
||||
'temperature_2m','precipitation_probability','weather_code',
|
||||
'wind_speed_10m','apparent_temperature'
|
||||
'wind_speed_10m','apparent_temperature','rain','snowfall'
|
||||
].join(','),
|
||||
daily: [
|
||||
'weather_code','temperature_2m_max','temperature_2m_min',
|
||||
'precipitation_sum','precipitation_probability_max',
|
||||
'wind_speed_10m_max','sunrise','sunset','uv_index_max'
|
||||
].join(','),
|
||||
forecast_days: 7,
|
||||
wind_speed_unit: 'mph',
|
||||
temperature_unit: 'fahrenheit',
|
||||
precipitation_unit: 'inch'
|
||||
forecast_days: 16,
|
||||
wind_speed_unit: useMetric ? 'kmh' : 'mph',
|
||||
temperature_unit: useMetric ? 'celsius' : 'fahrenheit',
|
||||
precipitation_unit: useMetric ? 'mm' : 'inch'
|
||||
});
|
||||
const r = await fetch(`https://api.open-meteo.com/v1/forecast?${params}`);
|
||||
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
|
||||
app.get('/api/radar/times', async (req, res) => {
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user