Add core app features for event management and notifications

- 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
This commit is contained in:
2026-05-13 08:37:52 +02:00
parent 316e5def72
commit 672d053ece
17 changed files with 1170 additions and 535 deletions
+123
View File
@@ -0,0 +1,123 @@
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 };
}