From 695c7a65a1e9ab3b95beeaf02a1e3b10bb16996b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 20:32:17 +0100 Subject: feat: client-side trip tracking with browser notifications (#151) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com> --- src/frontend/app/contexts/JourneyContext.tsx | 84 ++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/frontend/app/contexts/JourneyContext.tsx (limited to 'src/frontend/app/contexts') diff --git a/src/frontend/app/contexts/JourneyContext.tsx b/src/frontend/app/contexts/JourneyContext.tsx new file mode 100644 index 0000000..f513aa8 --- /dev/null +++ b/src/frontend/app/contexts/JourneyContext.tsx @@ -0,0 +1,84 @@ +import { + createContext, + useCallback, + useContext, + useRef, + useState, + type ReactNode, +} from "react"; + +export interface ActiveJourney { + tripId: string; + stopId: string; + stopName: string; + routeShortName: string; + routeColour: string; + routeTextColour: string; + headsignDestination: string | null; + /** Minutes remaining when tracking was started (for display context) */ + initialMinutes: number; + /** Send notification when this many minutes remain (default: 2) */ + notifyAtMinutes: number; + /** Whether the "approaching" notification has already been sent */ + hasNotified: boolean; +} + +interface JourneyContextValue { + activeJourney: ActiveJourney | null; + startJourney: ( + journey: Omit + ) => void; + stopJourney: () => void; + markNotified: () => void; +} + +const JourneyContext = createContext(null); + +export function JourneyProvider({ children }: { children: ReactNode }) { + const [activeJourney, setActiveJourney] = useState( + null + ); + const notificationRef = useRef(null); + + const startJourney = useCallback( + (journey: Omit) => { + // Close any existing notification + if (notificationRef.current) { + notificationRef.current.close(); + notificationRef.current = null; + } + setActiveJourney({ ...journey, hasNotified: false }); + }, + [] + ); + + const stopJourney = useCallback(() => { + if (notificationRef.current) { + notificationRef.current.close(); + notificationRef.current = null; + } + setActiveJourney(null); + }, []); + + const markNotified = useCallback(() => { + setActiveJourney((prev) => + prev ? { ...prev, hasNotified: true } : null + ); + }, []); + + return ( + + {children} + + ); +} + +export function useJourney() { + const ctx = useContext(JourneyContext); + if (!ctx) { + throw new Error("useJourney must be used within a JourneyProvider"); + } + return ctx; +} -- cgit v1.3