/* ═══════════════════════════════════════════════════════════════ NEXT EVENT COUNTDOWN A strip under the hero: the next event, and how long until it. The API picks the event (pinned in the admin, or the next one that hasn't ended); this works out the moment to count to. For a one-off event that's local midnight on starts_on — dates here are calendar dates with no time attached. For a series it's the next meeting, at the series' start time when it has one, so a weekly class counts down to Tuesday 7pm rather than to a start date months in the past. Once the moment passes and the event hasn't ended, the strip says it's happening now instead of counting below zero. The strip's heading is the event itself, so the section title and blurb from the admin aren't drawn here. ═══════════════════════════════════════════════════════════════ */ import { useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { eventHref } from '../../../lib/hrefs.ts' import { upcomingOccurrences } from '../../../lib/eventSeries.ts' import type { CountdownEvent } from '../../../lib/useFrontPage.ts' const TEAL = '#138ba0' /* Local midnight (or HH:MM) on a 'YYYY-MM-DD'. */ function localMoment(date: string, time?: string | null): Date | null { const [y, m, d] = date.split('-').map(Number) if (!y || !m || !d) return null const [hh, mm] = (time ?? '00:00').split(':').map(Number) return new Date(y, m - 1, d, hh || 0, mm || 0) } function target(event: CountdownEvent): Date | null { if (event.series) { const next = upcomingOccurrences(event.series, event.starts_on, event.ends_on, 1)[0] return next ? localMoment(next, event.series.start_time) : null } return event.starts_on ? localMoment(event.starts_on) : null } function useNow(intervalMs: number) { const [now, setNow] = useState(() => Date.now()) useEffect(() => { const timer = window.setInterval(() => setNow(Date.now()), intervalMs) return () => window.clearInterval(timer) }, [intervalMs]) return now } /* `overlap` tucks the strip up over the hero's bottom edge, which only makes sense when it's the first band after the hero. The admin can move it anywhere, so Home decides. */ export default function NextEventCountdown({ event, overlap, }: { event: CountdownEvent overlap: boolean }) { const now = useNow(1000) const when = target(event) const accent = event.color || TEAL const remaining = when ? when.getTime() - now : 0 const live = !when || remaining <= 0 const parts = [ ['days', Math.floor(remaining / 86_400_000)], ['hrs', Math.floor(remaining / 3_600_000) % 24], ['min', Math.floor(remaining / 60_000) % 60], ['sec', Math.floor(remaining / 1000) % 60], ] as const const where = event.location_label || (event.is_online ? 'Online' : null) return (

{live ? 'Happening now' : 'Next up'}

{event.title}

{[event.theme && `“${event.theme}”`, event.date_label, where] .filter(Boolean) .join(' · ')}

{!live && (
{parts.map(([label, value]) => (
{String(value).padStart(2, '0')} {label}
))}
)} Details →
) }