Files
time_to_leave/apps/mobile/src/hooks/useWienerLinien.ts
T
fegger bf252a9e9b Refactor mobile UI and centralize HAFAS parsing
Introduce useColors hook to replace direct theme usage in mobile
screens. Extract EventHeader, JourneyList, BikeSection, and
NearbyStops components to reduce complexity in EventDetailScreen.

Move parseHafasJourneys to packages/core for shared usage between
web and mobile clients. Update web API route with stricter HAFAS
validation and consistent client instantiation.

Add comprehensive codebase function guide documenting data flow,
shared packages, and service integrations.
2026-05-13 19:13:27 +02:00

108 lines
2.8 KiB
TypeScript

import { useState, useEffect, useRef, useCallback } from 'react';
import type { WienerLinienStop, WienerLinienDeparture } from '@timetoleave/core';
import { api } from '../services/api';
export interface DepartureRow {
stopId: string;
lineName: string;
direction: string;
minutes: number;
}
const DEBOUNCE_MS = 400;
const REFRESH_INTERVAL_MS = 60_000;
function transformDeparture(dep: WienerLinienDeparture): DepartureRow {
const minutes = Math.max(0, Math.round((dep.departureTime - Date.now()) / 60_000));
return {
stopId: dep.stopId,
lineName: dep.line.name,
direction: dep.direction,
minutes,
};
}
export function useWienerLinien(
lat: number | undefined,
lng: number | undefined,
radius?: number,
) {
const [stops, setStops] = useState<WienerLinienStop[]>([]);
const [departures, setDepartures] = useState<DepartureRow[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const stopIdsRef = useRef<string[]>([]);
const cancelledRef = useRef(false);
const fetchMonitor = useCallback(async (stopIds: string[]): Promise<void> => {
if (stopIds.length === 0 || cancelledRef.current) return;
try {
const rawDepartures = await api.monitorStops(stopIds);
if (!cancelledRef.current) {
setDepartures(rawDepartures.map(transformDeparture));
}
} catch {
// Silently ignore monitor errors — stops are still shown
}
}, []);
useEffect(() => {
cancelledRef.current = false;
if (lat === undefined || lng === undefined) {
setStops([]);
setDepartures([]);
setError(null);
setLoading(false);
return;
}
const debounceTimer = setTimeout(async () => {
if (cancelledRef.current) return;
setLoading(true);
setError(null);
try {
const stopsList = await api.findNearbyStops(lat, lng, radius ?? 500);
if (cancelledRef.current) return;
setStops(stopsList);
setLoading(false);
const ids = stopsList.map((s) => s.id);
stopIdsRef.current = ids;
await fetchMonitor(ids);
} catch (err) {
if (cancelledRef.current) return;
setError(err instanceof Error ? err.message : 'Haltestellen konnten nicht geladen werden');
setLoading(false);
}
}, DEBOUNCE_MS);
return () => {
cancelledRef.current = true;
clearTimeout(debounceTimer);
};
}, [lat, lng, radius, fetchMonitor]);
// Periodic departures refresh
useEffect(() => {
if (stops.length === 0) return;
const intervalId = setInterval(() => {
const ids = stopIdsRef.current;
if (ids.length > 0) {
fetchMonitor(ids);
}
}, REFRESH_INTERVAL_MS);
return () => clearInterval(intervalId);
}, [stops.length, fetchMonitor]);
return { stops, departures, loading, error };
}