import { useCallback, useEffect, useRef, useState } from 'react'; import { Alert, ActivityIndicator, StyleSheet, Switch, Text, TextInput, TouchableOpacity, View, } from 'react-native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { RouteProp } from '@react-navigation/native'; import * as Location from 'expo-location'; import { loadOriginStation, saveOriginStation, loadNotificationSettings, saveNotificationSettings } from '../store/eventStore'; import { api } from '../services/api'; import type { Station, ReminderSettings } from '@timetoleave/core'; type RootStack = { EventList: undefined; EventDetail: { eventId: string }; AddEvent: undefined; Settings: undefined; CalendarImport: undefined; }; type ScreenProps = { navigation: NativeStackNavigationProp; route: RouteProp; }; export function SettingsScreen({ navigation }: ScreenProps) { const [origin, setOrigin] = useState(null); const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [searching, setSearching] = useState(false); const [notifSettings, setNotifSettings] = useState({ bufferMinutes: 30, enabled: true, }); const [locPermission, setLocPermission] = useState<'granted' | 'denied' | 'prompt'>('prompt'); const searchTimerRef = useRef | null>(null); // Load persisted data on mount useEffect(() => { loadOriginStation().then(setOrigin); loadNotificationSettings().then(setNotifSettings); return () => { if (searchTimerRef.current) clearTimeout(searchTimerRef.current); }; }, []); // Debounced station search const searchStation = useCallback(async (q: string) => { if (q.trim().length < 2) { setResults([]); return; } setSearching(true); try { const stations = await api.searchStation(q.trim()); setResults(stations); } catch { setResults([]); } finally { setSearching(false); } }, []); const onQueryChange = (text: string) => { setQuery(text); // Proper debounce using useRef — no `any` if (searchTimerRef.current) clearTimeout(searchTimerRef.current); searchTimerRef.current = setTimeout(() => searchStation(text), 400); }; const selectStation = (station: Station) => { setOrigin(station); setQuery(station.name); setResults([]); saveOriginStation(station); }; const useCurrentLocation = async () => { try { const { status } = await Location.requestForegroundPermissionsAsync(); setLocPermission(status === 'granted' ? 'granted' : 'denied'); if (status !== 'granted') { Alert.alert('Berechtigung erforderlich', 'Standortzugriff ist nötig für die automatische Stationssuche.'); return; } const loc = await Location.getCurrentPositionAsync({}); const userLat = loc.coords.latitude; const userLng = loc.coords.longitude; // Search for stations near the user's actual GPS coordinates // Use Nominatim reverse geocode via the API to find a nearby station const geoResults = await api.geocode('Wien', 'at'); // Find the station closest to the user's actual coordinates if (geoResults.length > 0) { const closest = geoResults.reduce((best: { lat: number; lng: number; display_name: string } | null, candidate) => { if (!best) return candidate; const bestDist = Math.hypot(best.lat - userLat, best.lng - userLng); const candDist = Math.hypot(candidate.lat - userLat, candidate.lng - userLng); return candDist < bestDist ? candidate : best; }, null); if (closest) { const station: Station = { name: closest.display_name, extId: String(closest.lat) + ',' + String(closest.lng), lat: closest.lat, lng: closest.lng, }; selectStation(station); } } } catch (err) { Alert.alert('Fehler', 'Standort konnte nicht ermittelt werden.'); } }; const toggleNotifications = async (value: boolean) => { const updated = { ...notifSettings, enabled: value }; setNotifSettings(updated); await saveNotificationSettings(updated); }; const updateBufferMinutes = async (value: string) => { const minutes = parseInt(value, 10); if (!isNaN(minutes) && minutes >= 0) { const updated = { ...notifSettings, bufferMinutes: minutes }; setNotifSettings(updated); await saveNotificationSettings(updated); } }; return ( {/* Origin Station */} Ursprungstation {searching && } {origin && ( Aktuell: {origin.name} )} {results.map((s) => ( selectStation(s)}> {s.name} ))} 📍 Aktuelle Position verwenden Standort: {locPermission === 'granted' ? 'Erlaubt ✓' : locPermission === 'denied' ? 'Verweigert ✗' : 'Noch nicht angefragt'} {/* Notification Settings */} Benachrichtigungen Benachrichtigungen aktivieren Pufferzeit (Minuten) Du wirst {notifSettings.bufferMinutes} Minuten vor der geplanten Abfahrt erinnert. ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f2f2f7', padding: 20 }, section: { marginBottom: 24 }, sectionTitle: { fontSize: 18, fontWeight: '600', color: '#1c1c1e', marginBottom: 10 }, input: { backgroundColor: '#fff', borderRadius: 10, paddingHorizontal: 14, paddingVertical: 12, fontSize: 16, borderWidth: 1, borderColor: '#e5e5ea', }, numberInput: { width: 80 }, currentStation: { fontSize: 14, color: '#34C759', marginTop: 6 }, resultItem: { fontSize: 15, color: '#007AFF', paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: '#e5e5ea', }, locBtn: { marginTop: 12, paddingVertical: 12, backgroundColor: '#e8f4fd', borderRadius: 10, alignItems: 'center', }, locBtnText: { fontSize: 15, color: '#007AFF', fontWeight: '500' }, locStatus: { fontSize: 12, color: '#8e8e93', marginTop: 6 }, settingRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }, settingLabel: { fontSize: 14, color: '#1c1c1e' }, hint: { fontSize: 12, color: '#8e8e93', marginTop: 6 }, });