v1.4 - Cosmic Events

This commit is contained in:
fraxle
2026-05-16 21:18:28 +01:00
parent 64db88cb90
commit 214b8987cc
4 changed files with 939 additions and 1 deletions
+101
View File
@@ -31,6 +31,7 @@ import {
cloudCategory, confidenceBand, moonGlyph,
} from './utils.js';
import { SkyScope, WindVane, CloudIcon, ScopeReticle, PrecipIcon, CustomSelect, VentPill } from './components.js';
import { getActiveEvent, shouldShowCellTag, getUpcomingEvents } from './events.js';
const html = htm.bind(h);
@@ -718,6 +719,13 @@ export function UTCIForecast() {
? utciCategory(currentRow.utciAdj)
: { bg: '#4a4228', fg: '#ede4cc', label: 'No data' };
// ─── COSMIC/WEATHER EVENT ─────────────────────────────────────────────
// Compute once per render. todayRows = the rows for today (day 0).
const [dismissedEventId, setDismissedEventId] = useState(null);
const todayRows = days[0]?.rows || [];
const activeEvent = getActiveEvent(todayRows, location);
const showBanner = activeEvent && activeEvent.id !== dismissedEventId;
// ─── 4. JSX RETURN ───────────────────────────────────────────────────
// Everything below is the actual page markup, written as one big HTM
// template. Search tips:
@@ -758,6 +766,29 @@ export function UTCIForecast() {
</nav>
<main class="utci-shell">
<!-- ── EVENT BANNER ── slides down when an event is active ── -->
<div class=${`event-banner-wrap${showBanner ? ' visible' : ''}`}>
${showBanner && html`
<div class="event-banner" style=${{
background: activeEvent.color,
color: activeEvent.textColor,
}}>
<span class="event-banner-emoji">${activeEvent.emoji}</span>
<div class="event-banner-body">
<div class="event-banner-title">${activeEvent.title}</div>
<div class="event-banner-msg">${activeEvent.message}</div>
</div>
<button
class="event-banner-dismiss"
style=${{ color: activeEvent.textColor }}
onClick=${() => setDismissedEventId(activeEvent.id)}
aria-label="Dismiss"
title="Dismiss"
>✕</button>
</div>
`}
</div>
<div class="utci-header">
<div>
<h1 class="utci-title">
@@ -780,6 +811,7 @@ export function UTCIForecast() {
elev=${currentRow?.elev ?? 0}
dt=${currentRow?.dt ?? new Date()}
glob=${currentRow?.glob ?? 0}
activeEvent=${activeEvent}
/>
</div>
@@ -859,6 +891,9 @@ export function UTCIForecast() {
const dayName = i === 0 ? 'Today'
: i === 1 ? 'Tomorrow'
: dDate.toLocaleDateString('en-GB', { weekday: 'short', timeZone: 'UTC' });
const utcipVals = d.rows.map(r => r.utciAdj).filter(v => isFinite(v));
const dayHi = utcipVals.length ? Math.round(Math.max(...utcipVals)) : null;
const dayLo = utcipVals.length ? Math.round(Math.min(...utcipVals)) : null;
return html`
<button
key=${d.key}
@@ -893,6 +928,20 @@ export function UTCIForecast() {
<span class="utci-day-date" style=${{ color: '#5a3f24' }}>
${dDate.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' })}
</span>
${dayHi !== null && html`
<span style=${{
display: 'block',
fontFamily: 'Fraunces, serif',
fontStyle: 'italic',
fontSize: '11px',
marginTop: '3px',
letterSpacing: 0,
textTransform: 'none',
}}>
<span style=${{ color: '#c0622a', fontWeight: 600 }}>${dayHi}°</span>
<span style=${{ opacity: 0.5 }}> / </span>
<span style=${{ color: '#3a6080' }}>${dayLo}°</span>
</span>`}
</button>`;
})}
</div>
@@ -1408,6 +1457,9 @@ ${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['a
<span style=${{ display: 'inline-flex', alignItems: 'center', gap: '7px', verticalAlign: 'middle' }}>
<${SkyScope} elev=${r.elev} dt=${r.dt} glob=${r.glob} size=${30} />
<span>${localHHMM}</span>
${shouldShowCellTag(activeEvent, r) && html`
<span class="event-cell-tag" title=${activeEvent.title}>${activeEvent.emoji}</span>
`}
</span>
</td>
${visibleCols.air && html`<td style=${{ background: tempBg(r.Ta) }}>${r.Ta.toFixed(1)}</td>`}
@@ -1538,6 +1590,55 @@ ${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['a
</span>`)}
</div>
</div>
<!--
ALMANAC — upcoming cosmic events in the next 3 months.
Location-aware: visibility notes adjust by latitude.
-->
${(() => {
const upcoming = getUpcomingEvents(location, 90);
const formatPeak = (iso) => {
const d = new Date(iso + 'T00:00Z');
return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' });
};
const countdownLabel = (days) => {
if (days === 0) return 'Tonight!';
if (days === 1) return 'Tomorrow';
if (days < 7) return `In ${days} days`;
if (days < 14) return 'Next week';
if (days < 60) return `In ${Math.round(days / 7)} weeks`;
return `In ${Math.round(days / 30)} months`;
};
return html`
<div class="almanac-panel">
<div class="almanac-header">
<span class="almanac-title">🔭 What's Coming</span>
<span class="almanac-subtitle">Cosmic events · next 90 days · ${location.name}</span>
</div>
<div class="almanac-list">
${upcoming.length === 0
? html`<div class="almanac-empty">No major cosmic events in the next 90 days — clear skies ahead.</div>`
: upcoming.map(ev => html`
<div key=${ev.id} class="almanac-entry">
<div class="almanac-entry-accent" style=${{ background: ev.color === '#fdf8ee' ? '#c8922a' : ev.color }} />
<div class="almanac-entry-icon">${ev.emoji}</div>
<div class="almanac-entry-body">
<div class="almanac-entry-head">
<span class="almanac-entry-title">${ev.title}</span>
<span class="almanac-entry-date">${formatPeak(ev.peak)}</span>
<span class="almanac-entry-countdown">${countdownLabel(ev.daysUntil)}</span>
</div>
<div class="almanac-entry-desc">${ev.desc}</div>
${ev.visibilityNote && html`
<span class="almanac-entry-visibility">📍 ${ev.visibilityNote}</span>
`}
</div>
</div>
`)
}
</div>
</div>`;
})()}
</>`}
<div class="utci-about">