diff --git a/src/app/data/data-manager.ts b/src/app/data/data-manager.ts index 2e2ff583..6532054c 100644 --- a/src/app/data/data-manager.ts +++ b/src/app/data/data-manager.ts @@ -39,6 +39,7 @@ import { time, titlePlural, } from '../utils/utils'; +import { isEndingSoon } from '../utils/date-utils'; import { defaultMapRadius, distance, formatDistance, locationStringToPin, mapPointToPoint } from '../map/map.utils'; import { GpsCoord, Point, gpsToMap, mapToGps, setReferencePoints } from '../map/geo.utils'; import { set, get, clear } from 'idb-keyval'; @@ -263,6 +264,7 @@ export class DataManager implements WorkerClass { for (const event of this.events) { event.old = true; event.happening = false; + event.endingSoon = false; try { for (const occurrence of event.occurrence_set) { // This makes all events happen today @@ -274,14 +276,19 @@ export class DataManager implements WorkerClass { if (this.allEventsOld) { event.old = false; event.happening = false; + event.endingSoon = false; occurrence.old = false; occurrence.happening = false; + occurrence.endingSoon = false; hasLiveEvents = false; } else { - const isOld = new Date(occurrence.end_time).getTime() - todayTime < 0; - const isHappening = !isOld && new Date(occurrence.start_time).getTime() < todayTime; + const startTime = new Date(occurrence.start_time).getTime(); + const endTime = new Date(occurrence.end_time).getTime(); + const isOld = endTime - todayTime < 0; + const isHappening = !isOld && startTime < todayTime; occurrence.old = isOld; occurrence.happening = isHappening; + occurrence.endingSoon = isHappening && isEndingSoon(startTime, endTime, todayTime); if (!occurrence.old) { event.old = false; hasLiveEvents = true; @@ -289,6 +296,9 @@ export class DataManager implements WorkerClass { if (occurrence.happening) { event.happening = true; } + if (occurrence.endingSoon) { + event.endingSoon = true; + } } } } catch (err) { diff --git a/src/app/data/models.ts b/src/app/data/models.ts index 53c5badf..7b6cdb65 100644 --- a/src/app/data/models.ts +++ b/src/app/data/models.ts @@ -20,6 +20,7 @@ export interface Event { longTimeString: string; // Calculated old: boolean; // Calculated (whether the event has already passed) happening: boolean; // Calculated (whether the event is happening now) + endingSoon?: boolean; // Calculated (whether a current occurrence has less than 25% of its duration left) group?: string; // Calculated (grouping for favorites) distance: number; // Calculated distanceInfo: string; // Calculated @@ -74,6 +75,7 @@ export interface OccurrenceSet { start_time: string; old: boolean; // Calculated happening: boolean; // Calculated (whether the event is happening now) + endingSoon?: boolean; // Calculated (whether this occurrence has less than 25% of its duration left) longTimeString: string; // Calculated star?: boolean; } diff --git a/src/app/event/event.component.html b/src/app/event/event.component.html index d59e08da..dc918296 100644 --- a/src/app/event/event.component.html +++ b/src/app/event/event.component.html @@ -15,7 +15,9 @@
ENDING SOON
+ } @else if (event().happening) {NOW
}ENDING SOON
+ } @else if (event.happening) {NOW
} {{ items()[index].time }} diff --git a/src/app/utils/date-utils.test.ts b/src/app/utils/date-utils.test.ts index 67b58ab6..971abcbe 100644 --- a/src/app/utils/date-utils.test.ts +++ b/src/app/utils/date-utils.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { daysHighlighted, getTimeInTimeZone, toDate, getTimeZoneOffsetHours } from './date-utils'; +import { daysHighlighted, getTimeInTimeZone, isEndingSoon, toDate, getTimeZoneOffsetHours } from './date-utils'; describe('date-utils', () => { describe('toDate', () => { @@ -121,6 +121,35 @@ describe('date-utils', () => { }); }); + describe('isEndingSoon', () => { + const start = new Date('2024-08-28T13:00:00-07:00').getTime(); + const end = new Date('2024-08-28T17:00:00-07:00').getTime(); + + it('is true when less than 25% of a running event remains', () => { + const now = new Date('2024-08-28T16:30:00-07:00').getTime(); + expect(isEndingSoon(start, end, now)).toBe(true); + }); + + it('is false at exactly 25% remaining and true immediately after', () => { + const quarterLeft = new Date('2024-08-28T16:00:00-07:00').getTime(); + const underQuarter = new Date('2024-08-28T16:01:00-07:00').getTime(); + expect(isEndingSoon(start, end, quarterLeft)).toBe(false); + expect(isEndingSoon(start, end, underQuarter)).toBe(true); + }); + + it('is false before the event starts and once it ends', () => { + expect(isEndingSoon(start, end, start - 1)).toBe(false); + expect(isEndingSoon(start, end, end)).toBe(false); + expect(isEndingSoon(start, end, end + 1)).toBe(false); + }); + + it('rejects invalid and non-positive durations', () => { + expect(isEndingSoon(start, start, start)).toBe(false); + expect(isEndingSoon(end, start, start)).toBe(false); + expect(isEndingSoon(Number.NaN, end, start)).toBe(false); + }); + }); + describe('daysHighlighted', () => { it('should highlight all days for Burning Man 2024 event week', () => { // Real scenario: Burning Man 2024 dates (Aug 25 - Sept 2) diff --git a/src/app/utils/date-utils.ts b/src/app/utils/date-utils.ts index 9f1b818e..e67a1d4b 100644 --- a/src/app/utils/date-utils.ts +++ b/src/app/utils/date-utils.ts @@ -28,6 +28,22 @@ export function toDate(d: string | undefined): Date | undefined { return new Date(d); } +export function isEndingSoon(startTime: number, endTime: number, nowTime = Date.now()): boolean { + const duration = endTime - startTime; + if ( + !Number.isFinite(startTime) || + !Number.isFinite(endTime) || + !Number.isFinite(nowTime) || + duration <= 0 || + nowTime <= startTime || + nowTime >= endTime + ) { + return false; + } + + return endTime - nowTime < duration * 0.25; +} + export function getTimeZoneOffsetHours(timeZone: string): number { const date = new Date(); const timeZoneString = new Intl.DateTimeFormat('en-US', { timeZone, timeZoneName: 'longOffset' })