v1.41 - Add 2027 Dates and Fix Cosmic Events

This commit is contained in:
fraxle
2026-05-16 21:51:49 +01:00
parent 214b8987cc
commit b4d2a1427a
3 changed files with 450 additions and 144 deletions
+83 -31
View File
@@ -31,7 +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';
import { getActiveEvents, getLensEvent, getCellTagEvents, getUpcomingEvents } from './events.js';
const html = htm.bind(h);
@@ -719,12 +719,30 @@ 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);
// ─── COSMIC/WEATHER EVENTS ────────────────────────────────────────────
// activeEvents — today's events → drives banner & lens overlay.
// selectedDayEvents — viewed day's events → drives cell tags.
const [dismissedEventIds, setDismissedEventIds] = useState([]);
const [bannerIndex, setBannerIndex] = useState(0);
const todayRows = days[0]?.rows || [];
const activeEvent = getActiveEvent(todayRows, location);
const showBanner = activeEvent && activeEvent.id !== dismissedEventId;
const activeEvents = getActiveEvents(todayRows, location)
.filter(ev => !dismissedEventIds.includes(ev.id));
const lensEvent = getLensEvent(activeEvents);
const selectedDayEvents = getActiveEvents(visible, location);
// Auto-advance banner slideshow every 10 seconds when multiple events
useEffect(() => {
if (activeEvents.length <= 1) { setBannerIndex(0); return; }
const id = setInterval(() => {
setBannerIndex(i => (i + 1) % activeEvents.length);
}, 10000);
return () => clearInterval(id);
}, [activeEvents.length, activeEvents.map(e => e.id).join(',')]);
// Keep index in bounds if events change
useEffect(() => {
if (bannerIndex >= activeEvents.length) setBannerIndex(0);
}, [activeEvents.length]);
// ─── 4. JSX RETURN ───────────────────────────────────────────────────
// Everything below is the actual page markup, written as one big HTM
@@ -766,27 +784,61 @@ 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>
`}
<!-- ── EVENT BANNER ── slideshow when multiple events active ── -->
<div class=${`event-banner-wrap${activeEvents.length > 0 ? ' visible' : ''}`}>
${activeEvents.length > 0 && (() => {
const ev = activeEvents[bannerIndex] || activeEvents[0];
const fmtDate = (iso) => iso
? new Date(iso + 'T00:00Z').toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' })
: null;
const startFmt = fmtDate(ev.start);
const peakFmt = fmtDate(ev.peak);
const endFmt = fmtDate(ev.end);
// Only show date line if the event has start/end (cosmic events do; weather events don't)
const showDates = startFmt && endFmt;
const dateLine = showDates
? (startFmt === endFmt
? peakFmt ? `Peak: ${peakFmt}` : `Date: ${startFmt}`
: peakFmt && peakFmt !== startFmt && peakFmt !== endFmt
? `Active ${startFmt} ${endFmt} · Peak: ${peakFmt}`
: `Active ${startFmt} ${endFmt}`)
: null;
return html`
<div class="event-banner"
style=${{ background: ev.color, color: ev.textColor }}
onMouseEnter=${() => {}}
>
<span class="event-banner-emoji">${ev.emoji}</span>
<div class="event-banner-body">
<div class="event-banner-title">${ev.title}</div>
<div class="event-banner-msg">${ev.message}</div>
${dateLine && html`
<div class="event-banner-dates" style=${{ opacity: 0.75, fontSize: '11px', fontFamily: 'JetBrains Mono, monospace', letterSpacing: '0.08em', textTransform: 'uppercase', marginTop: '5px' }}>
${dateLine}
</div>
`}
${activeEvents.length > 1 && html`
<div class="event-banner-dots">
${activeEvents.map((_, i) => html`
<span
key=${i}
class=${`event-banner-dot${i === bannerIndex ? ' active' : ''}`}
onClick=${() => setBannerIndex(i)}
style=${{ background: ev.textColor }}
/>
`)}
</div>
`}
</div>
<button
class="event-banner-dismiss"
style=${{ color: ev.textColor }}
onClick=${() => setDismissedEventIds(ids => [...ids, ev.id])}
aria-label="Dismiss"
title="Dismiss this event"
></button>
</div>`;
})()}
</div>
<div class="utci-header">
@@ -811,7 +863,7 @@ export function UTCIForecast() {
elev=${currentRow?.elev ?? 0}
dt=${currentRow?.dt ?? new Date()}
glob=${currentRow?.glob ?? 0}
activeEvent=${activeEvent}
activeEvent=${lensEvent}
/>
</div>
@@ -1457,9 +1509,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>
`}
${getCellTagEvents(selectedDayEvents, r).map(ev => html`
<span key=${ev.id} class="event-cell-tag" title=${ev.title}>${ev.emoji}</span>
`)}
</span>
</td>
${visibleCols.air && html`<td style=${{ background: tempBg(r.Ta) }}>${r.Ta.toFixed(1)}</td>`}