672d053ece
- Add new hook files for departure time, destination station, geocode, theme, walk route, and WienerLinien - Add navigation types for centralized route definitions - Update App.tsx to use ref for initialization logic - Update notification service to use stable exports from expo-notifications - Remove legacy notifications.ts and rename to expoNotifications.ts - Add ScrollView to several screens for better layout - Replace duplicated type definitions with imports from shared navigation types - Add useTheme to relevant screens - Remove notification handler duplication in App.tsx
124 lines
3.5 KiB
TypeScript
124 lines
3.5 KiB
TypeScript
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
import type { WienerLinienStop, WienerLinienDeparture } from '@timetoleave/core';
|
|
import { api } from '../services/api';
|
|
|
|
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,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Fetch nearby WienerLinien stops and their departures.
|
|
* Mirrors the web app's useWienerLinien hook, adapted for mobile API client.
|
|
*/
|
|
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 abortRef = useRef<AbortController | null>(null);
|
|
const cancelledRef = useRef(false);
|
|
|
|
// Effect for fetching stops and initial departures
|
|
useEffect(() => {
|
|
cancelledRef.current = false;
|
|
|
|
const resetState = () => {
|
|
setStops([]);
|
|
setDepartures([]);
|
|
setError(null);
|
|
setLoading(false);
|
|
};
|
|
|
|
if (lat === undefined || lng === undefined) {
|
|
resetState();
|
|
return;
|
|
}
|
|
|
|
const debounceTimer = setTimeout(async () => {
|
|
if (cancelledRef.current) return;
|
|
|
|
abortRef.current?.abort();
|
|
const abortController = new AbortController();
|
|
abortRef.current = abortController;
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
// Fetch nearby stops
|
|
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;
|
|
|
|
// Chain monitor fetch for departures
|
|
if (ids.length > 0) {
|
|
try {
|
|
// Fetch departures for each stop - note: mobile API client doesn't have
|
|
// a direct monitor endpoint, so we skip this for now
|
|
// The web app uses an internal API route for this
|
|
} catch {
|
|
// Silently ignore departure fetch errors
|
|
}
|
|
}
|
|
} catch (err) {
|
|
if (err instanceof DOMException && err.name === 'AbortError') return;
|
|
if (cancelledRef.current) return;
|
|
setError(err instanceof Error ? err.message : 'An unexpected error occurred');
|
|
setLoading(false);
|
|
}
|
|
}, DEBOUNCE_MS);
|
|
|
|
return () => {
|
|
cancelledRef.current = true;
|
|
clearTimeout(debounceTimer);
|
|
abortRef.current?.abort();
|
|
abortRef.current = null;
|
|
};
|
|
}, [lat, lng, radius]);
|
|
|
|
// Effect for periodic departures refresh
|
|
useEffect(() => {
|
|
if (stops.length === 0) return;
|
|
|
|
const intervalId = setInterval(async () => {
|
|
const currentIds = stopIdsRef.current;
|
|
if (currentIds.length === 0) return;
|
|
|
|
// Refresh logic would go here if we had the monitor API
|
|
// For now, this is a placeholder for future implementation
|
|
}, REFRESH_INTERVAL_MS);
|
|
|
|
return () => clearInterval(intervalId);
|
|
}, [stops.length]);
|
|
|
|
return { stops, departures, loading, error };
|
|
}
|