aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/hooks/useJourneyTracker.ts
blob: 97bf23d60cf3deddf84cce136786dbe4587708e5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useStopArrivals } from "./useArrivals";
import { useJourney } from "../contexts/JourneyContext";

/**
 * Polls the stop arrivals for the active journey and fires a browser
 * notification when the tracked bus is approaching.
 *
 * Mount this hook once at the app-shell level so it continues tracking
 * even when the user navigates away from the stop page.
 */
export function useJourneyTracker() {
  const { t } = useTranslation();
  const { activeJourney, stopJourney, markNotified } = useJourney();

  const stopId = activeJourney?.stopId ?? "";
  const enabled = !!activeJourney;

  const { data } = useStopArrivals(stopId, false, enabled);

  // Keep a stable ref so the effect below doesn't re-run on every render
  const journeyRef = useRef(activeJourney);
  useEffect(() => {
    journeyRef.current = activeJourney;
  }, [activeJourney]);

  useEffect(() => {
    if (!data || !activeJourney) return;

    const journey = journeyRef.current;
    if (!journey) return;

    const arrival = data.arrivals.find((a) => a.tripId === journey.tripId);

    if (!arrival) {
      // Trip is no longer in the arrivals list — it has passed or expired
      stopJourney();
      return;
    }

    const { minutes, precision } = arrival.estimate;

    // Trip already departed from this stop
    if (precision === "past") {
      stopJourney();
      return;
    }

    // Fire approaching notification if not already sent
    if (!journey.hasNotified && minutes <= journey.notifyAtMinutes) {
      markNotified();

      if (
        typeof Notification !== "undefined" &&
        Notification.permission === "granted"
      ) {
        const title =
          minutes <= 0
            ? t("journey.notification_now_title", "¡Tu autobús está llegando!")
            : t("journey.notification_approaching_title", {
                defaultValue: "Tu autobús llega en {{minutes}} min",
                minutes,
              });

        const body = t("journey.notification_body", {
          defaultValue: "Línea {{line}} dirección {{destination}} — {{stop}}",
          line: journey.routeShortName,
          destination: journey.headsignDestination ?? "",
          stop: journey.stopName,
        });

        new Notification(title, {
          body,
          icon: "/icon-512.png",
          tag: `journey-${journey.tripId}`,
        });
      }
    }
  }, [data, activeJourney, markNotified, stopJourney, t]);
}