Skip to content

Commit f1d4735

Browse files
authored
Show next occurrence date for repeating upcoming events (#312)
Previous behavior was showing the first occurrence date, which is incorrect.
1 parent 48d37c2 commit f1d4735

File tree

1 file changed

+50
-8
lines changed

1 file changed

+50
-8
lines changed

src/components/UpcomingEvents.tsx

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,49 @@ import { transformEventsApiDates } from '../api/events';
66
import type { Event } from '../types/events';
77
import { Temporal } from 'temporal-polyfill';
88

9+
/**
10+
* For recurring events, advances `start` (and `end`) to the next occurrence
11+
* on or after today. Non-recurring events are returned unchanged.
12+
*/
13+
function getNextOccurrence(event: Event): Event {
14+
if (!event.repeats) return event;
15+
16+
const today = Temporal.Now.plainDateISO();
17+
const startDT = Temporal.PlainDateTime.from(event.start);
18+
const endDT = event.end ? Temporal.PlainDateTime.from(event.end) : null;
19+
const repeatEnds = event.repeatEnds
20+
? Temporal.PlainDateTime.from(event.repeatEnds)
21+
: null;
22+
23+
const intervalDays = event.repeats === 'weekly' ? 7 : 14;
24+
25+
const excludedDates = new Set(event.repeatExcludes?.map(String) ?? []);
26+
27+
const daysBehind = startDT.toPlainDate().until(today).days;
28+
const stepsNeeded = daysBehind > 0 ? Math.ceil(daysBehind / intervalDays) : 0;
29+
let current = startDT.add({ days: stepsNeeded * intervalDays });
30+
31+
// Skip any excluded dates
32+
while (excludedDates.has(current.toPlainDate().toString())) {
33+
current = current.add({ days: intervalDays });
34+
}
35+
36+
// If the next occurrence is past the recurrence end, keep the original
37+
if (repeatEnds && Temporal.PlainDateTime.compare(current, repeatEnds) > 0) {
38+
return event;
39+
}
40+
41+
const duration = endDT ? startDT.until(endDT) : null;
42+
43+
return {
44+
...event,
45+
start: current.toString({ smallestUnit: 'second' }),
46+
end: duration
47+
? current.add(duration).toString({ smallestUnit: 'second' })
48+
: event.end,
49+
};
50+
}
51+
952
function formatDate(dateStr: string): string {
1053
const date = new Date(dateStr);
1154
return date.toLocaleDateString('en-US', {
@@ -146,20 +189,19 @@ const UpcomingEvents = () => {
146189
useEffect(() => {
147190
const fetchEvents = async () => {
148191
try {
149-
const response = (
150-
await eventsApiClient.apiV1EventsGet({
151-
upcomingOnly: true,
152-
featuredOnly: true,
153-
})
154-
)
155-
.filter((x) => x.featured)
192+
const raw = await eventsApiClient.apiV1EventsGet({
193+
upcomingOnly: true,
194+
featuredOnly: true,
195+
});
196+
const response = transformEventsApiDates(raw.filter((x) => x.featured))
197+
.map(getNextOccurrence)
156198
.sort((a, b) => {
157199
const aStart = Temporal.PlainDateTime.from(a.start);
158200
const bStart = Temporal.PlainDateTime.from(b.start);
159201
return Temporal.PlainDateTime.compare(aStart, bStart);
160202
})
161203
.slice(0, 3);
162-
setFeaturedEvents(transformEventsApiDates(response));
204+
setFeaturedEvents(response);
163205
} catch (error) {
164206
console.error('Error fetching events:', error);
165207
} finally {

0 commit comments

Comments
 (0)