feat: add location search and update functionality in user settings

This commit is contained in:
2026-01-23 00:37:40 +01:00
parent 1a21280e08
commit 2b9a891462
3 changed files with 167 additions and 5 deletions
+136 -5
View File
@@ -1,13 +1,13 @@
import ThemedHeader from "@/src/components/themed/ThemedHeader";
import React from "react";
import React, {useState} from "react";
import ThemedBackground from "@/src/components/themed/ThemedBackground";
import ChangePasswordFeature from "@/src/components/ChangePasswordFeature";
import ThemeToggleButton from "@/src/components/ThemeToggleButton";
import SpotifyAuthButton from "@/src/components/SpotifyAuthButton";
import {restService, Token} from "@/src/services/RestService";
import {restService, Token, LocationResult} from "@/src/services/RestService";
import CustomModal from "@/src/components/themed/CustomModal";
import {useAuth} from "@/src/stores/authStore";
import {View, Text} from "react-native";
import {View, Text, TextInput, Pressable, ScrollView} from "react-native";
import ThemedButton from "@/src/components/themed/ThemedButton";
import {useRouter} from "expo-router";
@@ -15,6 +15,12 @@ export default function SettingsScreen() {
const {authenticatedUser, logout, refreshUser} = useAuth();
const router = useRouter();
const [locationQuery, setLocationQuery] = useState("");
const [locationResults, setLocationResults] = useState<LocationResult[]>([]);
const [isSearching, setIsSearching] = useState(false);
const [isLocationModalVisible, setIsLocationModalVisible] = useState(false);
const [hoveredLocation, setHoveredLocation] = useState<LocationResult | null>(null);
const handleAuthSuccess = (token: Token) => {
const spotifyConfig = {
accessToken: token.access_token,
@@ -31,6 +37,38 @@ export default function SettingsScreen() {
});
};
const searchLocation = async () => {
if (!locationQuery.trim()) return;
setIsSearching(true);
try {
const res = await restService.searchLocations(locationQuery.trim());
if (res.ok) {
setLocationResults(res.data.locations || []);
}
} catch (e) {
console.error("Location search failed", e);
} finally {
setIsSearching(false);
}
};
const applyLocation = async (loc: LocationResult) => {
try {
await restService.updateSelfLocation({ name: loc.name, lat: loc.lat, lon: loc.lon });
await refreshUser();
closeAndResetModal();
} catch (e) {
console.error("Location update failed", e);
}
};
const closeAndResetModal = () => {
setIsLocationModalVisible(false);
setLocationResults([]);
setHoveredLocation(null);
setLocationQuery("");
};
return (
<ThemedBackground>
<View className="flex-1 gap-6">
@@ -38,6 +76,25 @@ export default function SettingsScreen() {
Hallo, {authenticatedUser?.name}
</ThemedHeader>
<View className="bg-surface dark:bg-surface-dark rounded-2xl p-5">
<View className="flex-row items-center justify-between">
<View>
<Text className="text-base font-semibold text-onSurface dark:text-onSurface-dark mb-1">
Standort
</Text>
<Text className="text-sm text-muted dark:text-muted-dark">
Aktueller Standort: {authenticatedUser?.location?.name ?? "nicht gesetzt"}
</Text>
</View>
<Pressable
onPress={() => setIsLocationModalVisible(true)}
className="px-3 py-2 rounded-lg bg-primary/10 border border-primary/30"
>
<Text className="text-primary font-semibold"> Bearbeiten</Text>
</Pressable>
</View>
</View>
<View className="bg-surface dark:bg-surface-dark rounded-2xl p-5">
<Text className="text-base font-semibold text-onSurface dark:text-onSurface-dark mb-4">
Erscheinungsbild
@@ -96,8 +153,82 @@ export default function SettingsScreen() {
}}
/>
</View>
<CustomModal
isVisible={isLocationModalVisible}
onClose={closeAndResetModal}
className="bg-surface dark:bg-surface-dark max-w-md w-full self-center"
>
<View>
<View className="flex-row items-center justify-between mb-3">
<Text className="text-lg font-semibold text-onSurface dark:text-onSurface-dark">
Standort wählen
</Text>
<Pressable onPress={closeAndResetModal}>
<Text className="text-primary font-semibold">Schließen</Text>
</Pressable>
</View>
<TextInput
className="bg-background dark:bg-background-dark text-onSurface dark:text-onSurface-dark rounded-lg px-3 py-2 mb-3"
placeholder="Stadt suchen"
placeholderTextColor="#888"
value={locationQuery}
onChangeText={setLocationQuery}
onSubmitEditing={searchLocation}
returnKeyType="search"
/>
<ThemedButton
mode="contained"
title={isSearching ? "Suche..." : "Standort suchen"}
onPress={searchLocation}
disabled={isSearching}
/>
<ScrollView className="mt-4 max-h-64">
{locationResults.map((loc, idx) => (
<Pressable
key={`${loc.name}-${idx}`}
onPress={() => applyLocation(loc)}
onHoverIn={() => setHoveredLocation(loc)}
onHoverOut={() => setHoveredLocation(null)}
className="border border-outline dark:border-outline-dark rounded-lg p-3 mb-2"
>
<Text className="text-onSurface dark:text-onSurface-dark font-semibold">
{loc.name}
</Text>
<Text className="text-xs text-muted dark:text-muted-dark">
{loc.country ?? "Unbekannt"}{loc.state ? `, ${loc.state}` : ""}
</Text>
</Pressable>
))}
</ScrollView>
{locationResults.length > 0 && (
<View className="mt-3 p-3 rounded-lg bg-background dark:bg-background-dark border border-outline/60 dark:border-outline-dark/60">
<Text className="text-sm font-semibold text-onSurface dark:text-onSurface-dark mb-1">
Details
</Text>
<Text className="text-xs text-muted dark:text-muted-dark">
{hoveredLocation?.name ?? "—"}
</Text>
<Text className="text-xs text-muted dark:text-muted-dark">
{hoveredLocation
? `${hoveredLocation.country ?? "Land unbekannt"}${hoveredLocation.state ? `, ${hoveredLocation.state}` : ""}`
: "—"
}
</Text>
<Text className="text-xs text-muted dark:text-muted-dark">
{hoveredLocation
? `Koordinaten: ${hoveredLocation.lat.toFixed(4)}, ${hoveredLocation.lon.toFixed(4)}`
: "Fahre mit der Maus über einen Standort"
}
</Text>
</View>
)}
</View>
</CustomModal>
</View>
</ThemedBackground>
);
}
+6
View File
@@ -5,6 +5,12 @@ export interface User {
config: UserConfig,
lastState: MatrixState,
spotifyConfig: SpotifyConfig
location: {
name: string,
lat: number,
lon: number
},
timezone: string
}
export interface UserConfig {
+25
View File
@@ -26,6 +26,15 @@ export interface S3File {
size: number;
}
export interface LocationResult {
name: string;
lat: number;
lon: number;
country?: string;
state?: string;
local_names?: Record<string, string>;
}
// Token provider function type - will be set from authStore
type TokenProvider = () => string | null;
let tokenProvider: TokenProvider = () => null;
@@ -204,6 +213,22 @@ class RestService {
);
}
async searchLocations(query: string): Promise<ApiResponse<{ locations: LocationResult[] }>> {
return this.request<ApiResponse<{ locations: LocationResult[] }>>(
'GET',
`/location/search?q=${encodeURIComponent(query)}`
);
}
async updateSelfLocation(payload: { name: string; lat: number; lon: number }): Promise<ApiResponse<User>> {
return this.request<ApiResponse<User>>(
'PUT',
'/user/me/location',
payload,
{'Content-Type': 'application/json'}
);
}
private async request<T>(method: Method, url: string, data?: any, headers?: any): Promise<T> {
try {
const response = await this.api.request<T>({