// ------------------------------------------------------------------------ // events.js - Cosmic & weather event engine for SunScope. // // This is the public-facing barrel module. It composes the smaller files // under ./events/ and exposes the same API as before, so callers // (app.js, components.js) keep working without changes. // // Returns the "active event" (or null) based on: // 1. PROMO_OVERRIDE - manually set a message for promotions etc. // 2. Hardcoded cosmic calendar (eclipses, meteor showers, alignments) // 3. Weather-derived events (stargazing, sunset, heat spike, storm) // // To set a promo banner, edit PROMO_OVERRIDE below. // Set it back to null when done. // // Each event object: // { id, emoji, title, message, color, textColor, type } // // type: 'cosmic' | 'weather' | 'promo' // // Where to find things: // Cosmic calendar data ........ ./events/cosmic-calendar.js // Almanac calendar data ....... ./events/almanac-calendar.js // Weather check functions ..... ./events/weather-checks.js // Dynamic message helper ...... ./events/dynamic-message.js // Lens overlay SVG renderer ... ./events/lens-overlay.js // ------------------------------------------------------------------------ import { COSMIC_CALENDAR } from './events/cosmic-calendar.js'; import { checkStargazing, checkSunset, checkHeatSpike, checkStorm, checkFrost, } from './events/weather-checks.js'; import { dynamicCosmicMessage } from './events/dynamic-message.js'; // Re-export so callers that imported these from events.js keep working. export { getUpcomingEvents } from './events/almanac-calendar.js'; export { getLensOverlaySVG } from './events/lens-overlay.js'; // --- PROMO OVERRIDE ------------------------------------------------------ // Set this to show a custom banner regardless of weather or cosmic events. // Leave as null for automatic event detection. // // Example: // export const PROMO_OVERRIDE = { // id: 'promo-summer', // emoji: '--', // title: 'Summer Sale', // message: 'SunScope Extra - 20% off this weekend only.', // color: '#c8922a', // textColor: '#fff', // type: 'promo', // }; // export const PROMO_OVERRIDE = null; // --- CELL TAG LOGIC ------------------------------------------------------ // Returns the subset of active events that should show an icon for this row. export function getCellTagEvents(events, row) { if (!events || events.length === 0) return []; return events.filter(ev => { if (ev.type === 'promo') return false; if (ev.nightOnly && row.elev >= -5) return false; if (ev.id === 'heat-spike' && row.elev < 0) return false; // Sunset/sunrise events: only show icon during the actual twilight window if (ev.isoRange) { if (row.iso < ev.isoRange[0] || row.iso > ev.isoRange[1]) return false; } return true; }); } // --- MAIN EXPORT --------------------------------------------------------- // Returns ALL active events for the given rows/date as an array. // Empty array = nothing active. // // Order within array: // 1. PROMO_OVERRIDE (if set, returned alone) // 2. All matching cosmic calendar events for this date // 3. All matching weather-derived events export function getActiveEvents(rows, location) { // 1. Manual promo override - shown alone, no mixing with other events if (PROMO_OVERRIDE) return [PROMO_OVERRIDE]; // 2. Cosmic calendar - use the date of the rows being viewed, not today. const dateStr = (rows && rows.length > 0) ? rows[0].iso.slice(0, 10) : new Date().toISOString().slice(0, 10); const cosmicHits = COSMIC_CALENDAR .filter(ev => dateStr >= ev.start && dateStr <= ev.end) .map(ev => Object.assign({}, ev, { message: dynamicCosmicMessage(ev, dateStr) })); // 3. Weather-derived events (all that match, not just first) const weatherEvents = []; if (rows && rows.length > 0) { const checks = [ checkHeatSpike(rows), checkStorm(rows), checkFrost(rows), checkSunset(rows, location), checkStargazing(rows), ]; checks.forEach(ev => { if (ev) weatherEvents.push(ev); }); } return [...cosmicHits, ...weatherEvents]; } // --- LENS EVENT PICKER --------------------------------------------------- // From an array of active events, returns the single one closest to its peak // (most "intense"). Weather events without a peak use today's date. // Returns null if events array is empty. export function getLensEvent(events) { if (!events || events.length === 0) return null; if (events.length === 1) return events[0]; const today = new Date().toISOString().slice(0, 10); return events.reduce((best, ev) => { const peakB = best.peak || today; const peakE = ev.peak || today; const distB = Math.abs(new Date(today) - new Date(peakB)); const distE = Math.abs(new Date(today) - new Date(peakE)); return distE < distB ? ev : best; }); }