Files
time_to_leave/apps/web/src/hooks/useReminder.ts
T

64 lines
2.2 KiB
TypeScript

import { useEffect, useRef, useCallback } from "react";
import { useEventsStore } from "./useEventsStore";
import { useReminderSettings } from "./useReminderSettings";
const POLLS_MS = 30_000; // 30s — matches useClock cadence
/**
* Browser notification reminder engine. Polls every 30s and fires a
* `Notification` when an event's leave-by time is reached (bufferMinutes
* before the event). Uses a ref-based fired-set to avoid duplicate alerts
* for the same event across polls.
*/
export function useReminder() {
const { events } = useEventsStore();
const { bufferMinutes, enabled } = useReminderSettings();
const firedRef = useRef(new Set<string>());
const check = useCallback(() => {
if (!enabled) return;
if (typeof window === "undefined") return;
if (typeof Notification === "undefined") return;
const now = new Date();
const upcoming = events
.filter((e) => e.eventTime > now)
.sort((a, b) => a.eventTime.getTime() - b.eventTime.getTime());
for (const event of upcoming) {
const reminderTime = new Date(event.eventTime.getTime() - bufferMinutes * 60_000);
if (now >= reminderTime && !firedRef.current.has(event.id)) {
firedRef.current.add(event.id);
if (Notification.permission === "granted") {
new Notification("Time to leave!", {
body: `${event.title} starts in ${bufferMinutes} minutes`,
tag: `reminder-${event.id}`,
});
} else if (Notification.permission !== "denied") {
Notification.requestPermission().then((perm) => {
if (perm === "granted") {
new Notification("Time to leave!", {
body: `${event.title} starts in ${bufferMinutes} minutes`,
tag: `reminder-${event.id}`,
});
}
});
}
}
}
// Clear entries for events that no longer exist
const ids = new Set(upcoming.map((e) => e.id));
for (const id of firedRef.current) {
if (!ids.has(id)) firedRef.current.delete(id);
}
}, [events, bufferMinutes, enabled]);
useEffect(() => {
check();
const id = setInterval(check, POLLS_MS);
return () => clearInterval(id);
}, [check]);
}