Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/app/data/data-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -274,21 +276,29 @@ 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;
}
if (occurrence.happening) {
event.happening = true;
}
if (occurrence.endingSoon) {
event.endingSoon = true;
}
}
}
} catch (err) {
Expand Down
2 changes: 2 additions & 0 deletions src/app/data/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down
4 changes: 3 additions & 1 deletion src/app/event/event.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ <h1 style="display: flex">
<app-cached-img class="image" [alt]="event().title" [src]="event().imageUrl" />
</div>
}
@if (event().happening) {
@if (event().endingSoon) {
<p class="badge">ENDING SOON</p>
} @else if (event().happening) {
<p class="badge">NOW</p>
}
<ion-card-header>
Expand Down
4 changes: 3 additions & 1 deletion src/app/event/event.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ <h1 class="page-focus">{{event.title}}</h1>
<ion-item (click)="toggleStar(occurrence)">
<ion-icon aria-hidden="true" name="time-outline" slot="start"></ion-icon>
<ion-label>{{occurrence.longTimeString}}</ion-label>
@if (occurrence.happening) {
@if (occurrence.endingSoon) {
<app-badge>Ending Soon</app-badge>
} @else if (occurrence.happening) {
<app-badge>Now</app-badge>
} @if (!occurrence.happening) {
<ion-button aria-label="Make this event a favorite" color="primary" fill="clear" slot="end">
Expand Down
4 changes: 3 additions & 1 deletion src/app/events-card/events-card.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
[routerLink]="'/event/' + event.uid + '+' + 'Home'"
[lines]="items()[index].lines"
>
@if (event.happening) {
@if (event.endingSoon) {
<p class="badge">ENDING SOON</p>
} @else if (event.happening) {
<p class="badge">NOW</p>
}
<span slot="start">{{ items()[index].time }}</span>
Expand Down
31 changes: 30 additions & 1 deletion src/app/utils/date-utils.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions src/app/utils/date-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
Expand Down
Loading