bf252a9e9b
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.
108 lines
2.8 KiB
TypeScript
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 };
|
|
}
|